码迷,mamicode.com
首页 > 其他好文 > 详细

volatile

时间:2021-01-04 11:03:30      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:阶段   ati   行操作   轻量级   while   没有   技术   解决问题   不一致   

volatile

1.volatile保证可见性

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

技术图片
  1. 子线程t从主内存读取到数据放入其对应的工作内存

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

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

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

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

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

保证可见性的原理

技术图片

  • 写屏障(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)的值!显然这个值是不对的。

技术图片

但是加上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

技术图片

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

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

读写屏障可以参考这个面试题: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可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

volatile

标签:阶段   ati   行操作   轻量级   while   没有   技术   解决问题   不一致   

原文地址:https://www.cnblogs.com/dalianpai/p/14213706.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!