为什么有了synchronized,还需要volatile?

1 简述

  1. Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurrent包等;
  2. synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成;
  3. volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性;
  4. volatile无法保证原子性,synchronized可以保证被修饰的代码在同一时间只能被一个线程访问,保证了原子性;

2 sychronized的问题

2.1 性能损耗

  1. 虽然synchronized做了很多优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但它毕竟还是一种锁。无论是使用同步方法还是同步代码块,在同步操作之前需要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是有性能损耗的;
  2. 关于二者的性能对比,由于虚拟机对锁实行的许多消除和优化,使得我们很难量化这两者之间的性能差距,但是我们可以确定的一个基本原则是:volatile变量的读操作的性能与普通变量几乎无差别,写操作由于需要插入内存屏障所以会慢一些,即便如此,volatile在大多数场景下也比锁的开销要低。

2.2 产生阻塞

  1. 无论是ACC_SYNCHRONIZED(对应同步方法)还是monitorenter、monitorexit(对应同步代码块)都是基于Monitor实现的;
  2. 基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待;
    在这里插入图片描述
  3. 因此,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。而volatile是Java虚拟机提供的一种轻量级同步机制,它是基于内存屏障实现的,不会有阻塞锁带来的性能损耗问题;

3 volatile的附加功能(禁止指令重排)

volatile除了比synchronized性能好以外,还有一个很好的附加功能——禁止指令重排。

举例说明(单例模式)

不使用volatile:

public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
            if (singleton == null) {  
                singleton = new Singleton();  
            }  
        }  
    }  
    return singleton;  
    }  
}  

以上代码,我们通过使用synchronized对Singleton.class进行加锁,可以保证同一时间只有一个线程可以执行到同步代码块中的内容,也就是说singleton = new Singleton()这个操作只会执行一次,这就是实现了一个单例。

但是,当我们在代码中使用上述单例对象的时候有可能发生空指针异常。

假设Thread1 和 Thread2两个线程同时请求Singleton.getSingleton方法:

在这里插入图片描述

  • Step1:Thread1执行到第8行,开始进行对象的初始化;
  • Step2:Thread2执行到第5行,判断singleton == null;
  • Step3:Thread2经过判断发现singleton != null,所以执行第12行,返回singleton;
  • Step4:Thread2拿到singleton对象之后,开始执行后续的操作,比如调用singleton.call();

以上过程,看上去并没有什么问题,但是在Step4,Thread2在调用singleton.call()的时候,是有可能抛出空指针异常的,因为在Step3,Thread2拿到的singleton对象并不一定是一个完整的对象。

分析singleton = new Singleton()这行代码,其执行包括3个步骤:

  1. JVM为对象分配一块内存M;
  2. 在内存M上为对象进行初始化;
  3. 将内存M的地址赋值给singleton变量;

在这里插入图片描述
由于以上过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排成:

  1. JVM为对象分配一块内存M;
  2. 将内存的地址复制给singleton变量;
  3. 在内存M上为对象进行初始化;

在这里插入图片描述
也就是说,在Thread1还没有为对象进行初始化的时候,Thread2进来判断singleton==null就可能提前得到一个false,则会返回一个不完整的sigleton对象。

用volatile可以避免这个问题,因为volatile可以避免指令重排。

使用volatile:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
            if (singleton == null) {  
                singleton = new Singleton();  
            }  
        }  
    }  
    return singleton;  
    }  
}  

synchronized的有序性保证呢?

  1. Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的;
  2. as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变;
  3. 回到刚刚那个双重校验锁的例子,站在单线程的角度,也就是只看Thread1的话,因为编译器会遵守as-if-serial语义,所以这种优化不会有任何问题,对于这个线程的执行结果也不会有任何影响。但是,Thread1内部的指令重排却对Thread2产生了影响;

总结

volatile的重要性以及不可替代性:

  1. 一方面是因为synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题;
  2. 另外一方面,因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,在某些场景下有它的意义;

做并发控制的时候,如果不涉及到原子性的问题,可以优先考虑使用volatile关键字。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值