• 面试题: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可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
  • 相关阅读:
    城市的划入划出效果
    文本溢出省略解决笔记css
    长串英文数字强制折行解决办法css
    Poj 2352 Star
    树状数组(Binary Indexed Trees,二分索引树)
    二叉树的层次遍历
    Uva 107 The Cat in the Hat
    Uva 10336 Rank the Languages
    Uva 536 Tree Recovery
    Uva10701 Pre, in and post
  • 原文地址:https://www.cnblogs.com/dalianpai/p/14213706.html
Copyright © 2020-2023  润新知