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

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

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


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


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


JMM的内部工作机制


6.png


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


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


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


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


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


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


Volatile


Volatile 的特性


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


Volatile可见性


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


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


写操作:


7.png


读操作:


8.png


Volatile 禁止指令重排


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


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


9.png


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


10.png


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.返回对象引用


由于重排序的缘故,步骤2、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的唯一安全的情况是类中只有一个可变的域


各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!


目录
相关文章
|
2月前
|
存储 安全 Java
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
24 1
|
2月前
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
25 0
|
1月前
|
缓存 安全 Java
面试官:说说volatile应用和实现原理?
面试官:说说volatile应用和实现原理?
30 1
|
30天前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
1月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。
|
1月前
|
Java
【多线程面试题十五】、synchronized可以修饰静态方法和静态代码块吗?
这篇文章讨论了Java中的`synchronized`关键字是否可以修饰静态方法和静态代码块,指出`synchronized`可以修饰静态方法,创建一个类全局锁,但不能修饰静态代码块。
|
1月前
|
Java 调度
【多线程面试题十四】、说一说synchronized的底层实现原理
这篇文章解释了Java中的`synchronized`关键字的底层实现原理,包括它在代码块和方法同步中的实现方式,以及通过`monitorenter`和`monitorexit`指令以及`ACC_SYNCHRONIZED`访问标志来控制线程同步和锁的获取与释放。
|
1月前
|
Java
【多线程面试题十三】、说一说synchronized与Lock的区别
这篇文章讨论了Java中`synchronized`和`Lock`接口在多线程编程中的区别,包括它们在实现、使用、锁的释放、超时设置、锁状态查询以及锁的属性等方面的不同点。
|
1月前
|
缓存 Java 编译器
一文搞懂volatile面试题
这篇文章是关于Java关键字volatile的详细介绍和分析,volatile是多线程访问共享变量时保证一致性的方案,性能优于synchronized,但不保证操作原子性,需要同步处理。
|
2月前
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
55 4