辨析关键字volatile和synchronized

93 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情

辨析关键字volatile和synchronized

相信各位学习过JUC的小伙伴们,对volatile和synchronized这两个关键字肯定不陌生了,今天我们就一起来学习一下这两个关键字之间的区别。

话不多说,咱们马上开始!

关键字synchronized

这个其实是一个上锁的操作,使得锁住的代码能在并发环境下,一个个线程必须排队执行。

当然啦,这个底层是涉及到一个锁升级的概念的。

关键字volatile

我们都清楚volatile有以下几个著名的功能:

  1. 保持线程间的可见性
  2. 禁止指令重排序

volatile的底层其实是基于缓存一致性协议来实现的。

基于这个点,我们在这里也浅提一下缓存一致性协议的大概内容:

缓存一致性协议,针对缓存行

  1. 当第一个线程访问主存中的数据的时候,该缓存行标志为 E (独享的)
  2. 当有第二个线程访问该数据的时候,第一个线程是可以嗅探到的,这时候第一个线程会将标志有独占改为共享,与此同时,第二个线程的标志也是共享(S)
  3. 这时候假如第一个线程更改了该数据值,这时候,第一个线程中的对应缓存行标志改为 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. 分配对象内存(含默认值)
  2. 对象赋值(值)
  3. 将对象分配给实例(实例指向该对象)

本来如果指令是顺序执行的话,就一点问题都没有的,但是如果是指令顺序改变了,重排了;比如说变成了“1,3,2”,这时候,一个线程创建对象时,先执行了指令3,而指令2还没执行;如果这时候有别的线程来了,因为第一个线程已经执行了操作3了,所以该对象已经指向一个具体的地址了,这样会导致后来的线程判断该对象非空的,就直接使用了;而实际上该对象是还没创建完毕的,这样是会出问题的。