一、volatile 轻量级锁
synchronized 是阻塞式同步锁,valotile 在激烈竞争的情况下,会升级为重量级锁,有两个核心 (工作内存和主内存),三大性质:可见性,原子性,安全性。
二、 volatile 用途
多线程并发编程中,各个线程从共享变量的主内存拷贝到工作内存中,引擎会基于工作内存的数据进行处理。线程对volatile 变量的修改会立即被其他线程所感知
被volatile修饰的变量,能够使线程获得变量的最新值,从而避免出现脏数据的现象。
三、volatile 底层原理
1.生成了汇编代码,在对 volatile变量进行写操作的时候,JVM就会向处理器发送一条Lock前缀的指令,这些指令可以将当前处理器缓存的数据写入系统内存。使其他线程缓存的数据无效。当处理器发现这些数据无效时,会从系统内存中重新读取该数据,就可以获得最新值。
四、volatile 内存模型
五、volatile 应用场景
因 volatile 无法保证操作的原子性,所以使用时必须满足以下两个条件
1.对变量的写操作不能依赖于当前值
2.对变量没有包含在具有其他变量的不变式中,
1.状态标记量
volatile boolean flag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public void setFlag() {
flag = true;
}
2.单例模式
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
//new Singleton 主要做了三件事 1. 在堆内存中开辟内存 2. 通过构造函数初始化成员变量 3.将instance 对象 指向分配的内存空间。
六、volatile 深度解读
1.volatile 保证可见性
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
// 这个中标记方法不一定将线程1中断。因每个线程都有一个工作线程,线程1在运行的时候会将 stop 成员变量拷贝一份到工作内存中。
// 线程2更改stop的值还没来得及写入系统内存中,就去干其他事情了,线程1不知道线程2对 stop 变量的修改,还会一直循环下去。
//加入 volatile后
线程2对stop变量修改后,会强行的写入系统内存中,导致线程1中缓存的stop数据无效,所以线程1会重新从系统内存中读取新的数据。
2.volatile 保证原子性
//成员变量虽然保持了可见性,但是没有保证原子性,自增操作不能保持原子性 自增操作(读取变量的原始值,加1操作,写入工作内存)
//某个时刻,线程1对inc 进行自增操作,线程1读取了原始值,然后线程1被阻塞了,线程2进行了自增操作,也去读取变量的原始值。由于线程1对白能量inc
//进行了读取操作,却没有进行修改操作,所以线程2的缓存值不会失效,也不会时主内存中的值刷新。所以线程2会直接去主内存中读取值,进行加1操作,写入主内存。
//这时线程因已经进行读操作,还是之前的10,进行加1,等到11,最后写入内存,两个线程都进行了自增1的操作,但inc 却只加了 1,
//解决方案:
// 同意通过synchronized 或者 Lock 进行枷锁,保证原子性操作。或者使用AtomicInteger
public class Nothing {
private volatile int inc = 0;
private volatile static int count = 10;
private void increase() {
++inc;
}
public static void main(String[] args) {
int loop = 10;
Nothing nothing = new Nothing();
while (loop-- > 0) {
nothing.operation();
}
}
private void operation() {
final Nothing test = new Nothing();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000; j++) {
test.increase();
}
--count;
}).start();
}
// 保证前面的线程都执行完
while (count > 0) {
}
System.out.println("最后的数据为:" + test.inc);
}
}
最后的数据为:1206951
最后的数据为:43510
最后的数据为:0
最后的数据为:0
最后的数据为:2168433
最后的数据为:0
最后的数据为:72419
//CountDownLatch 使一个线程等待其他线程完成各自的工作后再执行
// 通过计数器实现,初始线程的任务数量,每个线程完成任务后,会自行减一,到零时,等待的线程可以恢复执行。
CountDownLatch countDownLatch = new CountDownLatch(100);
AtomicInteger atomicInteger = new AtomicInteger(0);//保证原子性操作。
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
atomicInteger.getAndIncrement();//自增一countDownLatch.countDown();//计数器减一
}
}.start();
}
//使当前线程等待,知道 计数器为 0
countDownLatch.await();
System.out.println(atomicInteger.get());
3.volatile 保证有序性
volatile关键字禁止指令重排序,,能在一定程度上保持安全性
1.1 当程序执行volatile变量的读操作或者写操作时,在其前面的操作的更改肯定已经全部执行完毕,结果对后面的线程可见。后面的操作肯定没有执行。
1.2 当进行指令优化时,不能将对volatile的读操作或者写操作语句放在其后面执行,也不能放在前面执行。
// x、y为非volatile变量
// flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
//在进行指令重排序的时候,不会将语句3 放在语句1,2的前面,也不会放在语句4,5的后面,volatile关键字保证执行到语句3时 语句 1,2是已经执行完毕的,
//语句1,2执行的结果时对 语句3,4,5时可见的。
七、乐观锁和悲观锁
在某个资源不可用的时候,就会让出cpu的资源,把当前线程状态切换为阻塞状态,等到资源可用了,就将线程唤醒。 这就是典型的悲观锁。
synchronized :是一种典型的独占锁 ,独占锁是一种悲观锁。认为一个线程修改共享数据的时候其他的线程也会修改数据。因此只会在其他线程不会干扰
的情况下 执行,这也会导致其他的线程挂起,等到持有锁的线程释放锁。
但是线程的挂起和执行的恢复都是有着很大的开销,当一个线程正在等待锁时,它不会做任何事情。所以这是悲观锁的很大的缺点。所以就有了 乐观锁的概念。
每次执行数据时不加锁,其他的线程一定不会修改,若是修改过产生冲突 就失败。重试。直到成功为止。乐观锁的效果更好。