Java实战小技巧(九):并发编程之volatile关键字

https://www.jianshu.com/p/157279e6efdb

1 volatile简介

在Java并发编程中,关键字volatile和synchronized常用于解决线程安全问题,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volatile则是Java虚拟机提供的最轻量级的同步机制。一个被volatile修饰的变量,能够保证每个线程获取该变量的最新值,从而避免出现数据脏读的现象。

通过研究Java内存模型可知,各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作。线程在工作内存进行操作后,何时会写到主内存对于普通变量是没有规定的,而针对volatile关键字修饰变量的修改会立刻被其他线程所感知,不会出现数据脏读的现象,从而保证共享数据的可见性。

2 实现原理

2.1 基本原理

生成汇编代码时,在volatile修饰的共享变量进行写操作时会多出Lock前缀的指令,影响如下:

  1. 将当前处理器缓存行的数据写回系统内存;
  2. 此写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条带Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据,来检查自己缓存的值是否已过期。当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。总结如下:

  1. 带Lock前缀的指令会引起处理器缓存写回内存;
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可获取当前最新值。

通过上述机制,可以使每个线程都能获得共享变量的最新值。

2.2 读写规则

在六条happens-before规则中,有一条是volatile变量规则:

对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

例如两个线程A和B同时操作一个volatile变量,如果线程A happens-before线程B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序。当线程A对volatile变量执行写操作后,线程B的本地内存中共享变量就会置为失效的状态,需要从主内存读取该变量的最新值。

横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,就像是给线程B发送了一个消息,告诉线程B现在共享变量的值已过期,然后当线程B读取该变量时,就像是接收了线程A刚刚发送的消息。

2.3 内存语义

为了性能优化,JMM在不改变正确语义的前提下,允许编译器和处理器对指令序列进行重排序,如果想阻止重排序,需要添加内存屏障。JMM内存屏障分类如下:

类型指令示例描述
LoadLoadLoad1–>Barrier–>Load2确保Load1数据装载先于Load2及之后所有装载指令。
StoreStoreStore1–>Barrier–>Store2确保Store1数据存储对其他处理器可见(刷新到内存)先于Store2及之后所有存储指令。
LoadStoreLoad–>Barrier–>Store确保Load数据装载先于Store及之后所有存储指令。
StoreLoadStore–>Barrier–>Load确保Store数据存储对其他处理器可见先于Load及之后所有数据装载指令。该类型屏障会使所有屏障之前的内存访问指令(包括装载和存储)执行完毕后,才执行屏障之后的指令。

为阻止重排序,JMM采取的策略如下:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障,可禁止前面的普通写和后面的volatile写重排序;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障,可防止前面的volatile写与和后面可能有的volatile读写重排序;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障,可禁止后面所有的普通读操作和前面的volatile读重排序;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障,可禁止后面所有的普通写操作和前面的volatile读重排序。;

volatile写操作是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。指令执行顺序如下:

普通读
普通写
StoreStore Barrier
volatile写
StoreLoad Barrier
volatile读
LoadLoad Barrier
LoadStore Barrier
普通读
普通写

3 应用示例

示例代码如下:

	private static volatile boolean isOver = false;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            int i = 1;
            System.out.println("线程开始工作...");
            while (!isOver) {
                try {
                    Thread.sleep(100);
                    System.out.println("线程执行第" + i + "次");
                    i++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束工作");
        });
        thread.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isOver = true;
    }

当子线程发现共享变量被主线程修改后,跳出循环。输出结果如下:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mars Coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值