1.volatile简介
synchronized是阻塞式同步,线程竞争下升级为重量级锁
volatile是轻量级锁,被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
2.volatile的作用
2.1 保证内存可见性
线程的共享变量都存储在主存中,每一个线程都有独有的工作内存,线程操作的变量是从主存中拷贝过来的副本放入工作内存,有一个工作内存中的副本发生变化就会刷回主内存中,其他线程中该变量的缓存作废,当要其他线程需要用时从主内存中读取该变量的值。
MESI缓存一致性协议
volatile可见性是通过汇编加上Lock
前缀指令触发底层的MESI
缓存一致性协议实现,该Lock
指令主要有2个作用:
- 当前处理器缓存行的数据写回系统内存
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
volatile实现原理:
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
MESI主要有以下4种状态:
状态 | 描述 |
---|---|
M 修改(Modified) | 此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存后 |
E 独享(Exclusive) | 此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作,如果发生,该缓存行需要变成共享状态 |
S 共享(Shared) | 此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使该缓存行无效的请求,如果发生,该缓存行需要变成无效状态 |
I 无效(Invalid) | 此时该缓存行无效 |
2.2禁止指令重排序
为保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:
- 为instance分配内存
- 初始化instance
- 将instance变量指向分配的内存空间
由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。
内存屏障
volatile有序性通过内存屏障实现,JVM和CPU都会对指令做重排序优化,所以在指令间插入一个屏障点,告诉JVM和CPU不能重排优化。具体分为 读读、读写、写读、写写4种屏障。
屏障点 | 描述 |
---|---|
每个volatile写的前面插入一个store-store屏障 | 禁止上面的普通写和下面的volatile写重排序 |
每个volatile写的后面插入一个store-load屏障 | 禁止上面的volatile写与下面的volatile读/写重排序 |
每个volatile读的后面插入一个load-load屏障 | 禁止下面的普通读和上面的volatile读重排序 |
每个volatile读的后面插入一个load-store屏障 | 禁止下面的普通写和上面的volatile读重排序 |
2.3 不保证原子性
public class Test {
private static CountDownLatch countDownLatch = new CountDownLatch(1000);
private volatile static int num = 0;
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
num++;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
System.out.println(num);
}
}
使用volatile关键字修饰num,开启1000个线程但是num的值最后< 1000。假设2个线程A、B,线程A获取num=1,执行num++
时候发现线程B已经将num修改为4,所以根据MESI协议A的num值失效,重新从主内存中读取最新值
上述过程A、B线程都做了+1操作,但是只有B成功了,也就是做了2次num++
,但只加了一次,所以num终值< 1000
解决原子性问题
使用synchronized重量级锁或者使用JUC包下的AtomicInteger
public class Test {
private static CountDownLatch countDownLatch = new CountDownLatch(1000);
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
num.getAndIncrement();
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
System.out.println(num);
}
}
3 volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
4 总结
volatile
关键字是轻量级锁
主要作用:
- 可见性(MESI缓存一致性协议)
- 有序性,禁止指令重排(内存屏障)
适用场景:一个线程写,多个线程读
注意:volatile
不是原子性操作