volatile关键字 和 可见性

内容纲要

volatile关键字

volatile是什么

  • volatile是一种同步机制 ,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
  • 如果一个变量修饰成volatile ,那么JVM就知道了这个变量可能会被并发修改
  • 但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安 全的,但是volatile做不到synchronized那样的原子保护, volatile仅在很有限的场景下才能发挥作用。

volatile的适用场合

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

volatile的作用:可见性、禁止重排序

  1. 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一 个volatile属性会立即刷入到主内存
  2. 禁止指令重排序优化:解决单例双重锁乱序问题

volatile和synchronized的关系?

  • volatile在这方面可以看做是轻量版的synchronized 轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

volatile小结

  1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
  5. volatile提供了happens-before保证, 对volatile变量V的写入happens-before所有其他线程后续对v的读操作。
  6. volatile可以使得long和double的赋值是原子的,后面马上会讲long和double的原子性。

能保证可见性的措施

  • 除了volatile可以让变量保证可见性外, synchronized、Lock、并发集合、Thread.join()和Thread.start() 等都可以保证的可见性
  • 具体看happens-before原则的规定

升华:对synchronized可见性的正确理解

  • synchronized不仅保证了原子性,还保证了可见性
  • synchronized不仅让被保护的代码安全,还近朱者赤

原则性

什么是原子性

  • 系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一 半的情况,是不可分割的。
  • ATM里取钱
  • i++ 不是原子性
  • synchronized实现原子性
  • 原子性并不是单指单一的操作,也可以一系列操作,当着一系列操作满足要么全部执行成功,要么全部不执行,不会出现执行一 半的情况,也是具有原子性

Java中的原子操作有哪些?

  • 除long和double之外的基本类型(int, byte, boolean, short, char,float)的赋值操作
  • 所有引|用reference的赋值操作,不管是32位的机器还是64位的机器
  • java.concurrent.Atomic.*包中所有类的原子操作

long和double的原子性

  • 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决
  • 结论:在32位上的JVM.上, long和double的操作不是原子的,但是在64位的JVM.上是原子的

原子操作+原子操作!=原子操作

  • 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
  • 比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给女朋友,也就是被其他线程打断并被修改。
  • 全同步的HashMap也不完全安全

单列模式的8种写法

  1. 饿汉式(静态常量)可用]
/**
 * 描述:     饿汉式(静态常量)(可用)
 */
public class Singleton1 {

    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

}
  1. 饿汉式(静态代码块) [可用]

    /**
    * 描述:     饿汉式(静态代码块)(可用)
    */
    public class Singleton2 {
    
    private final static Singleton2 INSTANCE;
    
    static {
        INSTANCE = new Singleton2();
    }
    
    private Singleton2() {
    }
    
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
    }
  2. 懒汉式(线程安全,同步方法)[不推荐用]
/**
 * 描述:     懒汉式(线程不安全)
 */
public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}
  1. 懒汉式(线程不安全)[不可用]
/**
 * 描述:     懒汉式(线程安全)(不推荐)
 */
public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {

    }

    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}
  1. 懒汉式(线程安全,同步方法)[不推荐用]
/**
 * 描述:     懒汉式(线程不安全)(不推荐)
 */
public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}
  1. 双重检查[推荐用,面试, Spring源码中有使用]

    • 优点:线程安全;延迟加载;效率较高。
    • 为什么要double-check
      1. 线程安全
      2. 单check行不行?
      3. 性能问题
    • 为什么要用volatile
      1. 新建对象实际上有3个步骤(可能会被jvm指令重排序)
      2. 新建一个空的Person对象
      3. 把这个对象的地址指向p
      4. 执行Person的构造函数
      5. 重排序会带来NPE
      6. 防止重排序
        
        /**
    • 描述: 双重检查(推荐面试使用)
      */
      public class Singleton6 {
      // 加上volatile,保证可见性
      private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
    if (instance == null) {
    synchronized (Singleton6.class) {
    if (instance == null) {
    instance = new Singleton6();
    }
    }
    }
    return instance;
    }
    }

  2. 静态内部类[推荐用,懒汉模式]

/**
 * 描述:     静态内部类方式,可用
 */
public class Singleton7 {

    private Singleton7() {
    }

    private static class SingletonInstance {

        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
  1. 枚举[推荐用]
/**
 * 描述:     枚举单例
 */
public enum Singleton8 {
    INSTANCE;

    public void whatever() {

    }
}
Singleton8.INSTANCE.whatever();

不同对比

  • 饿汉:简单,但是没有lazy loading
  • 懒汉:有线程安全问题
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:最好

用哪种单例的实现方案最好?

  • Joshua Bloch大神在《Effective Java)》中明确表达过的观点: "使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
  • 写法简单
  • 线程安全有保障
  • 避免反序列化破坏单例

各种写法的适用场合

  • 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象;
  • 非线程同步的方法不能使用;
  • 如果程序一开始要加载的资源太多, 那么就应该使用懒加载;
  • 饿汉式如果是对象的创建需要配置文件就不适用。
  • 懒加载虽然好,但是静态内部类这种方式会引|入编程复杂性
THE END
分享
二维码
< <上一篇
下一篇>>