5.volatile

假如想象 提交于 2020-01-19 09:20:34

1. 并发开发中的变量不可见问题

1.1. 问题现象

public class VolatileTest {

    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(()->{
            while (true) {
                if (flag) {
                    System.out.println("ThreadA : flag is " + flag);
                    break;
                }
            }
            System.out.println("ThreadA End");
        });
        Thread threadB = new Thread(()->{
            flag = true;
            System.out.println("ThreadB : flag is " + flag);
        });

        threadA.start();
        Thread.sleep(1000l);//为了保证threadA比threadB先启动,sleep一下
        threadB.start();
        
    }
}

使用 javap -v反编译class文件

     0: getstatic     #2                  // Field org/sumanit/concurrency/VolatileTest.flag:Z     获取静态字段 flag 的值 并将其压入栈顶
     3: ifeq          0                                                                            当栈顶 int 型数值等于 0 时跳转到第0行
     6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      获取静态字段 System.out 的值 并将其压入栈顶
     9: new           #4                  // class java/lang/StringBuilder                         new StringBuilder 对象 并将其引用值压入栈顶
    12: dup                                                                                        复制栈顶 StringBuilder 对象引用 并将复制值压入栈顶     
    13: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V           初始化 StringBuilder 对象
    16: ldc           #6                  // String ThreadA : flag is                              将 "ThreadA : flag is" 推送至栈顶
    18: invokevirtual #7                  // Method java/lang/StringBuilder.append:...             调用StringBuilder的append方法
    21: getstatic     #2                  // Field org/sumanit/concurrency/VolatileTest.flag:Z     获取静态字段 flag 的值 并将其压入栈顶
    24: invokevirtual #8                  // Method java/lang/StringBuilder.append:...             调用StringBuilder的append方法
    27: invokevirtual #9                  // Method java/lang/StringBuilder.toString:...           调用StringBuilder的toString方法
    30: invokevirtual #10                 // Method java/io/PrintStream.println:...                调用打印方法println
    33: goto          36                                                                           跳转到36行
    36: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;      获取静态字段 System.out 的值 并将其压入栈顶
    39: ldc           #11                 // String ThreadA End                                    将 "ThreadA : flag is" 推送至栈顶
    41: invokevirtual #10                 // Method java/io/PrintStream.println:                   调用打印方法println
    44: return

执行结果

ThreadB : flag is true

按照代码来看,当线程B启动后,线程A里的循环应该结束,输出ThreadA End,但是并没有,这是因为线程B对flag的修改没有同步给线程A,线程A无法看到。

1.2. 问题产生的原因

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2 或其他)后再进行操作,但操作完不知道何时会写到内存。

在上面的程序中有以下两种情况:

  1. ThreadA读取数据每次可能 都是在缓存中读取的,看不到ThreadB设置的值
  2. ThreadB写数据每次也只是写到缓存中,不会同步到主内存中

1.3. MESI MSI MOS Synapse Cache 一致性协议

2. 使用 volatile 解决变量不可见问题

public class VolatileTest {

    public static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(()->{
            while (true) {
                if (flag) {
                    System.out.println("ThreadA : flag is " + flag);
                    break;
                }
            }
            System.out.println("ThreadA End");
        });
        Thread threadB = new Thread(()->{
            flag = true;
            System.out.println("ThreadB : flag is " + flag);
        });

        threadA.start();
        Thread.sleep(1000l);//为了保证threadA比threadB先启动,sleep一下
        threadB.start();
        
    }
}

执行结果

ThreadB : flag is true
ThreadA : flag is true
ThreadA End

给变量增加 volatile 即可让变量线程可见并禁用指令重排

3. volatile原理

3.1. volatile 的特性

  • 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 有序性:禁止进行指令重排序。(实现有序性)
  • 原子性:volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

3.2. volatile 的实现原则

  1. Lock 前缀指令会引起处理器缓存回写到内存
    Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占任何共享内存[2]。但是,在最近的处理器里,LOCK# 信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。在8.1.4节有详细说明锁定操作对处理器缓存的影响,对于 Intel486 和 Pentium 处理器,在锁操作时,总是在总线上声言 LOCK# 信号。但在 P6 和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK# 信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效
    IA-32 处理器和 Intel 64 处理器使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和 Intel 64 处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在 Pentium 和 P6 family 处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充

3.3. volatile在字节码中的实现

public static volatile boolean flag;
descriptor: Z
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

在赋值和取值时并无特殊,但是多了一个ACC_VOLATILE ,应该是在运行时起作用的

3.4. volatile有序性的实现

volatile变量操作语句前面的所有操作语句都会在volatile变量操作语句执行前执行

volatile变量操作语句后面的所有操作语句都会在volatile变量操作语句执行后执行

而执行顺序的保证是在volatile语句前后插入内存屏障来实现的,下面是基于保守策略的 JMM 内存屏障插入策略:

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

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的 volatile 内存语义

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!