面试:为了进阿里,从新翻阅了Volatile与Synchronized

该系列文章已收录在公众号【Ccww技术博客】,原创技术文章早于博客推出

在深刻理解使用Volatile与Synchronized时,应该先理解明白Java内存模型 (Java Memory Model,JMM)java


Java内存模型(Java Memory Model,JMM)

Java内存(JMM)模型是在硬件内存模型基础上更高层的抽象,它屏蔽了各类硬件和操做系统对内存访问的差别性,从而实现让Java程序在各类平台下都能达到一致的并发效果。安全

JMM的内部工做机制多线程

image.png

  • 主内存:存储共享的变量值(实例变量和类变量,不包含局部变量,由于局部变量是线程私有的,所以不存在竞争问题)
  • 工做内存:CPU中每一个线程中保留共享变量的副本,线程的工做内存,线程在变动修改共享变量后同步回主内存,在变量被读取前从主内存刷新变量值来实现的。
  • 内存间的交互操做:不一样线程之间不能直接访问不属于本身工做内存中的变量,线程间变量的值的传递须要经过主内存中转来完成。(lock,unlock,read,load,use,assign,store,write)

JMM内部会有指令重排,而且会有af-if-serial跟happen-before的理念来保证指令的正确性并发

  • 为了提升性能,编译器和处理器经常会对既定的代码执行顺序进行指令重排序
  • af-if-serial:无论怎么重排序,单线程下的执行结果不能被改变
  • 先行发生原则(happen-before):先行发生原则有不少,其中程序次序原则,在一个线程内,按照程序书写的顺序执行,书写在前面的操做先行发生于书写在后面的操做,准确地讲是控制流顺序而不是代码顺序

Java内存模型为了解决多线程环境下共享变量的一致性问题,包含三大特性,app

  • 原子性:操做一旦开始就会一直运行到底,中间不会被其它线程打断(这操做能够是一个操做,也能够是多个操做),在内存中原子性操做包括read、load、user、assign、store、write,若是须要一个更大范围的原子性可使用synchronized来实现,synchronized块之间的操做。
  • 可见性:一个线程修改了共享变量的值,其它线程能当即感知到这种变化,修改以后当即同步回主内存,每次读取前当即从主内存刷新,可使用volatile保证可见性,也可使用关键字synchronized和final。
  • 有序性:在本线程中全部的操做都是有序的;在另外一个线程中,看来全部的操做都是无序的,就可须要使用具备自然有序性的volatile保持有序性,由于其禁止重排序。

在理解了JMM的时,来说讲Volatile与Synchronized的使用,Volatile与Synchronized到底有什么做用呢?jvm


Volatile

Volatile 的特性性能

  • 保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)
  • volatile 只能保证对单次读/写的原子性,i++ 这种操做不能保证原子性

Volatile可见性

当写一个volatile变量时,JMM会把该线程对应的工做内存中的共享变量值更新后刷新到主内存,优化

当读取一个volatile变量时,JMM会把该线程对应的工做内存置为无效,线程会从主内存中读取共享变量。this

写操做:
spa

读操做:

Volatile 禁止指令重排

JMM对volatile的禁止指令重排采用内存屏障插入策略:

在每一个volatile写操做的前面插入一个StoreStore屏障。在每一个volatile写操做的后面插入一个StoreLoad屏障

在每一个volatile读操做的后面插入一个LoadLoad屏障。在每一个volatile读操做的后面插入一个LoadStore屏障

Synchronized

Synchronized是Java中解决并发问题的一种最经常使用的方法,也是最简单的一种方法。Synchronized的做用主要有三个:

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改可以及时可见,实际上是经过Java内存模型中的 “对一个变量unlock操做以前,必需要同步到主内存中;若是对一个变量进行lock操做,则将会清空工做内存中此变量的值,在执行引擎使用此变量前,须要从新从主内存中load操做或assign操做初始化变量值” 来保证的
  • 有序性:有效解决重排序问题,即 “一个unlock操做先行发生(happen-before)于后面对同一个锁的lock操做”;

Synchronized总共有三种用法:

  1. 当synchronized做用在实例方法时,监视器锁(monitor)即是对象实例(this);
  2. 当synchronized做用在静态方法时,监视器锁(monitor)即是对象的Class实例,由于Class数据存在于永久代,所以静态方法锁至关于该类的一个全局锁;
  3. 当synchronized做用在某一个对象实例时,监视器锁(monitor)即是括号括起来的对象实例;

更加详细的解析看[Java并发之Synchronized]()

理解了Volatile与Synchronized后,那咱们来看看如何使用Volatile与Synchronized优化单例模式


单例模式优化-双重检测DCL(Double Check Lock)

先来看看通常模式的单例模式:

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}

    public static Singleton getInstance(){
            if(singleton == null){
                singleton = new Singleton();   // 建立实例
        }
        return singleton;
    }

}

可能出现问题:当有两个线程A和B,

  • 线程A判断if(singleton == null)准备执行建立实例时,线程挂起,
  • 此时线程B也会判断singleton为空,接着执行建立实例对象返回;
  • 最后,因为线程A已进入也会建立了实例对象,这就致使多个单例对象的状况

首先想到是那就在使用synchronized做用在静态方法:

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

虽然这样简单粗暴解决,但会致使这个方法比较效率低效,致使程序性能严重降低,那是否是还有其余更优的解决方案呢?

能够进一步优化建立了实例以后,线程再同步锁以前检验singleton非空就会直接返回对象引用,而不用每次都在同步代码块中进行非空验证,

若是只有synchronized前加一个singleton非空,就会出现第一种状况多个线程同时执行到条件判断语句时,会建立多个实例

所以须要在synchronized后加一个singleton非空,就不会出现会建立多个实例,

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

这个优化方案虽然解决了只建立单个实例,因为存在着指令重排,会致使在多线程下也是不安全的(当发生了重排后,后续的线程发现singleton不是null而直接使用的时候,就会出现意料以外的问题。)。致使缘由singleton = new Singleton()新建对象会经历三个步骤:

  • 1.内存分配
  • 2.初始化
  • 3.返回对象引用

因为重排序的缘故,步骤二、3可能会发生重排序,其过程以下:

  • 1.分配内存空间
  • 2.将内存空间的地址赋值给对应的引用
  • 3.初始化对象

那么问题找到了,那怎么去解决呢?那就禁止不容许初始化阶段步骤2 、3发生重排序,恰好Volatile 禁止指令重排,从而使得双重检测真正发挥做用。

public class Singleton {
    //经过volatile关键字来确保安全
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){
           synchronized (Singleton.class){
                if(singleton == null){
                singleton = new Singleton();
            }
        }
    }
    return singleton;
    }
}

最终咱们这个完美的双重检测单例模式出来了


总结

  • volatile本质是在告诉jvm当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取; synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则能够保证变量的修改可见性和原子性
  • volatile不会形成线程的阻塞;synchronized可能会形成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化
  • 使用volatile而不是synchronized的惟一安全的状况是类中只有一个可变的域
各位看官还能够吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
欢迎关注公众号【Ccww技术博客】,原创技术文章第一时间推出