zoukankan      html  css  js  c++  java
  • 面试题:volatile

    volatile

    1.volatile保证可见性

    代码比较简单,我就不贴出来了。

    image-20201230210234850
    1. 子线程t从主内存读取到数据放入其对应的工作内存

    2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存

    3. 此时main方法main方法读取到了flag的值为false

    4. 当子线程t将flag的值写回去后,失效其他线程对此变量副本

    5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

    总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

    保证可见性的原理

    image-20201230210531853

    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

    2.禁止指令重排序

    问题代码示例:

    /**
     * @author WGR
     * @create 2020/12/30 -- 21:10
     */
    public class OutOfOrderDemo06 {
        // 新建几个静态变量
        public static int a = 0 , b = 0;
        public static int i = 0 , j = 0;
    
        public static void main(String[] args) throws Exception {
            int count = 0;
            while(true){
                count++;
                a = 0 ;
                b = 0 ;
                i = 0 ;
                j = 0 ;
                // 定义两个线程。
                // 线程A
                Thread t1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        a = 1;
                        i = b;
                    }
                });
    
                // 线程B
                Thread t2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        b = 1;
                        j = a;
                    }
                });
    
                t1.start();
                t2.start();
                t1.join(); // 让t1线程优先执行完毕
                t2.join(); // 让t2线程优先执行完毕
    
                // 得到线程执行完毕以后 变量的结果。
                System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
                if(i == 0 && j == 0){
                    break;
                }
            }
        }
    }
    
    

    发生了重排序:在线程1和线程2内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并不是严格按照代码顺序执行的,他们的顺序改变了,这样就是发生了重排序,这里颠倒的 是 a = 1 ,i = b 以及j=a , b=1 的顺序,从而发生了指令重排序。直接获取了i = b(0) , j = a(0)的值!显然这个值是不对的。

    image-20201230211316988

    但是加上volatile关键字就会解决问题。
    按照happens-before规则,我们只需要给b加上volatile,那么b之前的写入( a = 3;)将对读取b之后的代码可见,也就是说即使a不加volatile,只要b读取到3,那么b之前的操作(a=3)就一定是可见的,此时就绝对不会出现b=3的时候而读取到a=1了。

    happens-before规则可以看我这个面试题:https://www.cnblogs.com/dalianpai/p/14212690.html

    image-20201230211610408

    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    读写屏障可以参考这个面试题:https://www.cnblogs.com/dalianpai/p/14162021.html

    3. volatile在双重检查加锁的单例中的应用

    单例概述

    • 单例是需要在内存中永远只能创建一个类的实例,
    • 单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。

    单例模式的适用场景:

    • 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
    • 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。

    单例模式有8种
    单例模式我们可以提供出8种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念。

    • 饿汉式单例的含义是:在获取单例对象之前对象已经创建完成了。
    • 懒汉式单例是指:在真正需要单例的时候才创建出该对象。

    饿汉单例的2种写法
    特点:在获取单例对象之前对象已经创建完成了。

    饿汉式(静态常量)
    /**
        目标:饿汉式(静态常量)
    
        步骤:
            1.构造器私有。
            2.定义一个静态常量保存一个唯一的实例对象(单例)
            3.
     */
    public class Singleton01 {
        // 2.定义一个静态常量保存一个唯一的实例对象(单例)
        private static final Singleton01 INSTANCE = new Singleton01();
        // 1.构造器私有。
        private Singleton01(){
    
        }
        // 3.提供一个方法返回单例对象。
        public static Singleton01 getInstance(){
            return INSTANCE;
        }
    }
    
    class Test01{
        public static void main(String[] args) {
            Singleton01 s1 = Singleton01.getInstance();
            Singleton01 s2 = Singleton01.getInstance();
            System.out.println(s1 == s2);
        }
    }
    
    饿汉式(静态代码块)
    /**
        目标:饿汉式(静态代码块)
    
        步骤:
            1.构造器私有。
            2.定义一个静态常量保存一个唯一的实例对象(单例),可以通过静态代码块初始化单例对象。
            3.提供一个方法返回单例对象。
     */
    public class Singleton02 {
        // 2.定义一个静态常量保存一个唯一的实例对象(单例)
        private static final Singleton02 INSTANCE ;
    
        static{
            INSTANCE = new Singleton02();
        }
    
        // 1.构造器私有。
        private Singleton02(){
    
        }
        // 3.提供一个方法返回单例对象。
        public static Singleton02 getInstance(){
            return INSTANCE;
        }
    }
    
    class Test02{
        public static void main(String[] args) {
            Singleton02 s1 = Singleton02.getInstance();
            Singleton02 s2 = Singleton02.getInstance();
            System.out.println(s1 == s2);
        }
    }
    

    懒汉式单例4种写法
    特点:在真正需要单例的时候才创建出该对象。在Java程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,程序员可能会采用延迟初始化。值得注意的是:要正确的实现线程安全的延迟初始化还是需要一些技巧的,否则很容易出现问题。

    懒汉式(线程不安全)
    /**
        目标:懒汉式(线程不安全的写法)。
        步骤:
            1.构造器私有。
            2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
            3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
                没有就创建一个新的单例对象。
     */
    public class Singleton03 {
        // 2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
        private static Singleton03 INSTANCE;
        // 1.构造器私有。
        private Singleton03(){
    
        }
        // 3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
        //   没有就创建一个新的单例对象。
        public static Singleton03 getInstance(){
            if(INSTANCE == null){
                // 说明这是第一次来拿单例对象,需要真正的创建出来!
                INSTANCE = new Singleton03();
            }
            return INSTANCE;
        }
    }
    
    
    懒汉式(线程安全,性能差)

    使用synchronized关键字修饰方法包装线程安全,但性能差多,并发下只能有一个线程正在进入获取单例对象。

    /**
        目标:懒汉式(线程安全的写法)。
        步骤:
            1.构造器私有。
            2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
            3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
                没有就创建一个新的单例对象。
            4.为获取单例的方法加锁:用synchronized
     */
    public class Singleton04 {
        // 2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
        private static Singleton04 INSTANCE;
        // 1.构造器私有。
        private Singleton04(){
    
        }
        // 3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有直接返回。
        //   没有就创建一个新的单例对象。
        // 懒汉式线程安全的写法:线程A , 线程B.
        public synchronized static Singleton04 getInstance(){
            if(INSTANCE == null){
                // 说明这是第一次来拿单例对象,需要真正的创建出来!
                INSTANCE = new Singleton04();
            }
            return INSTANCE;
        }
    }
    
    
    懒汉式(线程不安全)

    特点:是一种优化后的似乎线程安全的机制。

    /**
        目标:懒汉式(线程不安全)
    
        步骤:
            1.构造器私有。
            2.定义一个静态变量存储一个单例对象。
            3.提供一个方法返回一个单例对象。
    
    
     */
    public class Singleton05 {
        // 2.定义一个静态变量存储一个单例对象。
        private static Singleton05 INSTANCE ;
        // 1.构造器私有
        private Singleton05(){
    
        }
        // 3.返回一个单例对象
        public static Singleton05 getInstance(){
            // 判断单例对象的变量是否为null
            if(INSTANCE == null){
                // 很多个线程执行到这里来:A , B
                synchronized (Singleton05.class){
                    INSTANCE = new Singleton05();
                }
            }
            return INSTANCE;
        }
    }
    
    
    懒汉式(volatile双重检查模式,推荐)
    /**
        目标:双重检查机制,以及使用volatile修饰(最好,最安全的方式,推荐写法)
    
        步骤:
            1.构造器私有。
            2.提供了一个静态变量用于存储一个单例对象。
            3.提供一个方法进行双重检查机制返回单例对象。
            4.必须使用volatile修饰静态的变量。?
    
         双重检查的优点:线程安全,延迟加载,效率较高!!
     */
    public class Singleton06 {
        //  2.提供了一个静态变量用于存储一个单例对象。
        private volatile static Singleton06 INSTANCE;
    
        // 1.构造器私有。
        private Singleton06(){
    
        }
    
        // 3.提供一个方法进行双重检查机制返回单例对象。
        public static Singleton06 getInstance(){
            // 第一次检查:判断单例对象的变量是否为null
            if(INSTANCE == null ){
                // A , B
                synchronized (Singleton06.class){
                    // 第二次检查:判断单例对象的变量是否为null
                    if(INSTANCE == null){
                        INSTANCE = new Singleton06();
                    }
                }
            }
            return INSTANCE;
        }
    }
    
    
    静态内部类单例方式

    引入:JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案

    /**
        目标:基于类的初始化实现延迟加载和线程安全的单例设计。
    
        步骤:
            1.构造器私有。
            2.提供一个静态内部类,里面提供一个常量存储一个单例对象。
            3.提供一个方法返回静态内部类中的单例对象。
     */
    public class Singleton07 {
        //  1.构造器私有。
        private Singleton07(){
    
        }
        //  2.提供一个静态内部类,里面提供一个常量存储一个单例对象。
        private static class Inner{
            private static final Singleton07 INSTANCE = new Singleton07();
        }
    
        // .提供一个方法返回静态内部类中的单例对象。
        // 线程A , 线程B
        public static Singleton07 getInstance(){
            return Inner.INSTANCE;
        }
    }
    
    
    1. 静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。
    2. 通过对比基于volatile的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于volatile的双重检查锁定方案有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。
    枚举实现单例
    
    /**
        目标:枚举实现单例。
    
        引入:枚举实际上是一种多例的模式。如果我们直接定义一个实例就相当于是单例了。
     */
    public enum Singleton08 {
        INSTANCE;
    }
    

    4.小结

    应用场景
    • 赋值操作,volatile不适合做a++等操作。 如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

    • 触发器,按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见。

    volatile和synchronized区别
    • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
    • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
    • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
    volatile的总结
    • volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得修改后的值,比如boolean flag ;或者作为触发器,实现轻量级同步。
    • volatile属性的读写操作都是无锁的,它不能替代synchronized ,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
    • volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
    • volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
    • volatile提供了happens-before保证,对volatile变量v的写入happens- before所有其他线程后续对v的读操作。
    • volatile可以使得long和double的赋值是原子的。
    • volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
  • 相关阅读:
    LOJ
    LOJ
    LOJ
    一种树形背包的时间复杂度证明
    [机器学习]第四、五周记录
    [机器学习]第三周记录
    [家里训练20_02_16]C
    [机器学习]第二周记录
    wireshark无响应的问题
    [机器学习]第一周记录
  • 原文地址:https://www.cnblogs.com/dalianpai/p/14213706.html
Copyright © 2011-2022 走看看