由浅入深讲解volatile

volatile

volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略

volatile 的特性

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  2. 禁止进行指令重排序。(实现有序性)
  3. volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。关于volatile 原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步,就跟下面的SoWhat跟SynSoWhat功能类似哦。
class SoWhat {
        volatile int i = 0;// volatile修饰的变量
        public int getI() {
            return i;// 单个volatile变量的读
        }

        public void setI(int j) {
            this.i = j; // 单个volatile 变量的写
        }

        public void inc() {
            i++;//复合多个volatile 变量
        }
    }

    class SynSoWhat {
        int i = 0;

        public synchronized int getI() {
            return i;
        }

        public synchronized void setI(int j) {
            this.i = j;
        }

        public void inc() {
            // 普通方法调用
            int tmp = getI();
            // 调用已同步方法
            tmp = tmp + 1;
            //普通写方法
            setI(tmp);
            // 调用已同步方法
        }
    }

写理解

volatile写的内存语义如下:

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

public class VolaSemanteme {
        int a = 0;
        volatile boolean flag = false;

        // 这是重点哦 
        public void init() {
            a = 1;
            flag = true;
            //....... 
        }

        public void use() {
            if (flag) {
                int i = a * a;
            }
            //....... 
        }
    }

线程A调用init方法,线程B调用use方法。

由浅入深讲解volatile_第1张图片

 

volatile 指令重排

volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。关于内存屏障的具体讲解以前写过不再重复,JMM装逼于无形这里说过。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:

  1. 第一个为读操作时,第二个任何操作不可重排序到第一个前面。
  2. 第二个为写操作时,第一个任何操作不可重排序到第二个后面。
  3. 第一个为写操作时,第二个的读写操作也不运行重排序。

由浅入深讲解volatile_第2张图片

转存失败重新上传取消转存失败重新上传取消

volatile写底层实现

JMM对volatile的内存屏障插入策略

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

由浅入深讲解volatile_第3张图片

转存失败重新上传取消转存失败重新上传取消

volatile 读底层

JMM对volatile的内存屏障插入策略

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

由浅入深讲解volatile_第4张图片

转存失败重新上传取消转存失败重新上传取消

重点说下volatile读后面为什么跟了个LoadLoad。加入我有如下代码 AB两个线程执行,B线程的flag获取下面的读被提前了。

由浅入深讲解volatile_第5张图片

由浅入深讲解volatile_第6张图片

 

volatile的实现原理

有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。在CPU级别的功能如下:

将当前处理器缓存行的数据写回到系统内存

这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。

我们可以通过jitwatch对简单的代码进行详细的反汇编看一下。

public class VolaSemanteme {
        int unvloatileVal = 0;
        volatile boolean flag = false;

        public static void main(String[] args) {
            VolaSemanteme volaSemanteme = new VolaSemanteme();
            volaSemanteme.init();
            volaSemanteme.use();
        }

        public void init() {
            unvloatileVal = 1;
            flag = true; // 第九行哦
        }

        public void use() {
            if (flag) {
                int LocalA = unvloatileVal;
                if (LocalA == 0) {
                    throw new RuntimeException("error");
                }
            }
        }
    }

对普通变量的赋值操作:

由浅入深讲解volatile_第7张图片

 

对volatile变量的赋值操作。

由浅入深讲解volatile_第8张图片

 

可以对比得出,volatile 修饰的变量确实会多一个 lock addl $0x0,(%rsp) 指令。

0x0000000114ce95cb: lock addl $0x0,(%rsp) ;*putfield flag; - com.sowhat.demo.VolaSemanteme::init@7 (line 9)

使用 volatile 必须具备的条件

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。只有在状态真正独立于程序内其他内容时才能使用 volatile。

状态标志

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

 volatile boolean shutdownRequested;

        // ......

        public void shutdown() {

            shutdownRequested = true;

        }

        public void doWork() {

            while (!shutdownRequested) {

                // do stuff

            }

        }

 

一次性安全发布(one-time safe publication)

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

    public class BackgroundFloobleLoader {

        public volatile Flooble theFlooble;

        public void initInBackground() {
            // do lots of stuff
            theFlooble = new Flooble();// this is the only write to theFlooble
        }

    }


  public class SomeOtherClass {
        public void doWork() {
            while (true) {
                // do some stuff
                //...
                // use the Flooble, but only if it is ready
                if (floobleLoader.theFlooble != null)
                    doSomething(floobleLoader.theFlooble);
            }

        }
    }

模独立观察(independent observation)

安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

 public class UserManager {
        public volatile String lastUser;

        public boolean authenticate(String user, String password) {
            boolean valid = passwordIsValid(user, password);
            if (valid) {
                User u = new User();
                activeUsers.add(u);
                lastUser = user;
            }
            return valid;
        }
    }

volatile bean 模式

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

  @ThreadSafepublic
    class Person {
        private volatile String firstName;
        private volatile String lastName;
        private volatile int age;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

开销较低的读-写锁策略

volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafepublic
    class CheesyCounter {
        // Employs the cheap read-write lock trick
        // All mutative operations MUST be done with the 'this' lock
        held@GuardedBy("this")
        private volatile int value;

        public int getValue() {
            return value;
        }

        public synchronized int increment() {
            return value++;
        }
    }

双重检查(double-checked)

单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。

    class Singleton {
        private volatile static Singleton instance;

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

推荐懒加载优雅写法 Initialization on Demand Holder(IODH)。

    public class Singleton {
        public static Singleton getInstance() {
            return SingletonHolder.instance;
        }

        static class SingletonHolder {
            static Singleton instance = new Singleton();
        }
    }

 

你可能感兴趣的:(Java技术【备忘】)