在Java内存模型一节,除了synchronized外,我们还提到一个常用关键词----volatile,我们说过volatile保证了并发环境的可见性和顺序性,使用volatile修饰的变量,当然值发生改变时,可以同步到其他线程,其他线程按新值进行计算,下面我们编写代码来看下volatile关键词的使用和实现原理,验证我们前面抛出的结论。

当我们使用多线程对共享变量进行操作时,如果不用volatile,会是什么现象呢?以线程一节描述的使用标识位停止线程执行为例说明,代码如下:

public class VolatileTest {
    // 标志位
    boolean mInterrupted = false;
  
    // 设置标志位为true,打断线程运行
    public void needInterrupt(boolean isNeedInterrupt) {
        this.mInterrupted = isNeedInterrupt;
        System.out.println("修改mInterrupted值为:" + mInterrupted + ",time:" + System.currentTimeMillis());
    }

    public static void main(String[] args) {
        VolatileTest test = new VolatileTest();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "开始执行,time:" + System.currentTimeMillis());
            while (!test.mInterrupted) {
            }
            System.out.println(Thread.currentThread().getName() + "结束执行,time:" + System.currentTimeMillis());
        }, "CustomThread1").start();

        new Thread(() -> {
            try {
                Thread.sleep(2000);
                test.needInterrupt(true);
                System.out.println(Thread.currentThread().getName() + "打断关联的线程执行,time:" + System.currentTimeMillis());
            } catch (InterruptedException e) {
            }
        }, "CustomThread2").start();
    }
}

随后运行,输出如下:

Java-volatile实现详解(从java到汇编)-LMLPHP

可以看出在设置标志位为true后,很长一段时间仍未打印CustomThread1停止运行的日志,换而言之,我们虽然在CustomThread2中修改了mInterrupted的值为true,但是CustomThread1中值仍然为false,明显不符合我们预期,那么加上volatile修饰又会怎样呢?修改mInterrupted声明,添加volatile修饰,运行结果如下:

Java-volatile实现详解(从java到汇编)-LMLPHP

可以看出在使用volatile修饰后,在CustomThread2中执行打断后,CustomThread1立即停止运行了。

volatile实现原理

在synchronized实现原理中,我们了解到可以从字节码和汇编码两个角度去了解synchronized的实现,对于volatile,我们不妨也做相同假设,首先我们获取VolatileTest的字节码,看是否有相关实现,字节码代码如下:

Java-volatile实现详解(从java到汇编)-LMLPHP

从字节码可以看出,针对mInterrupted变量而言,除了声明时多了一个ACC_VOLATILE标记,在使用过程中无明显变化。

看来字节码中并没有volatile的核心实现,我们继续使用hsdis获取VolatileTest类的汇编码,进一步确定其实现,字节码如下:

Java-volatile实现详解(从java到汇编)-LMLPHP

可以看到在更新mInterrupted值时,增加了lock addl $0x0,(%rsp)指令,该指令就使得当前对共享变量的修改对其他线程立即生效,而lock addl是内核中的写屏障指令,所以我们一般说volatile关键词底层是通过内存屏障指令实现的。在有volatile修饰的变量需要赋值时,处理器会插入内存屏障指令,将当前线程工作内存的值刷新进主存,同时使得其他线程工作内存中引用的该变量的缓存地址失效,重新从主存同步新值,完成业务逻辑。

volatile与synchronized

volatile与synchronized区别见下表:

04-30 05:17