持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情
辨析关键字volatile和synchronized
相信各位学习过JUC的小伙伴们,对volatile和synchronized这两个关键字肯定不陌生了,今天我们就一起来学习一下这两个关键字之间的区别。
话不多说,咱们马上开始!
关键字synchronized
这个其实是一个上锁的操作,使得锁住的代码能在并发环境下,一个个线程必须排队执行。
当然啦,这个底层是涉及到一个锁升级的概念的。
关键字volatile
我们都清楚volatile有以下几个著名的功能:
- 保持线程间的可见性
- 禁止指令重排序
volatile的底层其实是基于缓存一致性协议来实现的。
基于这个点,我们在这里也浅提一下缓存一致性协议的大概内容:
缓存一致性协议,针对缓存行
- 当第一个线程访问主存中的数据的时候,该缓存行标志为 E (独享的)
- 当有第二个线程访问该数据的时候,第一个线程是可以嗅探到的,这时候第一个线程会将标志有独占改为共享,与此同时,第二个线程的标志也是共享(S)
- 这时候假如第一个线程更改了该数据值,这时候,第一个线程中的对应缓存行标志改为 M (修改),同时会发出信号让第二个线程将对应的标志位改为 I (无效的)。被标志了无效之后,当第二个线程想再次访问该数据的时候,就需要重新从主存中加载。
两个关键字的辨析例子
有了volatile,为什么还需要synchronized?
看到这里,可能会有些朋友有疑问了,既然volatile是基于缓存一致性协议来实现的,而在缓存一致性协议中明显是可以保证线程安全啊(一个线程修改了,另外一个线程再次读取的时候,强制读取最新值)。。。
那既然这样,volatile是不是可以替代synchronized呢?
其实答案肯定是不可以的,因为volatile(缓存一致性协议)只是保证了单一读取指令的原子性,而不是所有的原子性(很多时候,一行代码会对应很多个操作的)
下面举个例子:
public class T8_NotGuaranteeAtomicity implements Runnable{
/*
volatile可以保证单个读取操作的原子性(因为缓存一致性协议)
但是对于下面例子中的自增、自减操作不保证原子性
因为自增、自减操作是可以细分成多个指令的:
1.读取变量值
2.操作变量值
3.写回变量值
在执行了操作1之后,后面紧接着是操作2、操作3;而不会再次回去执行读取操作的
也正因为这样才引发了并发问题
*/
private /*volatile*/ int count = 100;
@Override
public /*synchronized*/ void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T8_NotGuaranteeAtomicity t = new T8_NotGuaranteeAtomicity();
for(int i=0; i<100; i++) {
new Thread(t, "T" + i).start();
}
}
}
有了synchronized,为什么需要volatile?
public class T9_VolatileInSinglePattern {
private volatile static T9_VolatileInSinglePattern t9;
private T9_VolatileInSinglePattern() {
}
public static T9_VolatileInSinglePattern getInstance() {
if (t9 == null) {
synchronized (T9_VolatileInSinglePattern.class) {
if (t9 == null) {
t9 = new T9_VolatileInSinglePattern();
}
}
}
return t9;
}
}
其实这种情况下,volatile最主要的作用就是禁止指令重排序!
创建对象可以分为以下三种指令:
- 分配对象内存(含默认值)
- 对象赋值(值)
- 将对象分配给实例(实例指向该对象)
本来如果指令是顺序执行的话,就一点问题都没有的,但是如果是指令顺序改变了,重排了;比如说变成了“1,3,2”,这时候,一个线程创建对象时,先执行了指令3,而指令2还没执行;如果这时候有别的线程来了,因为第一个线程已经执行了操作3了,所以该对象已经指向一个具体的地址了,这样会导致后来的线程判断该对象非空的,就直接使用了;而实际上该对象是还没创建完毕的,这样是会出问题的。