阅读 117

Java内存模型(JMM)和volatile

JMM介绍

JMM是为了保证多个线程之间可以有效的、正确的地协同工作而出现的。JMM的关键技术点都是围绕多线程的原子性,可见性和有序性来建立的。

Java内存模型的抽象结构示意图如下:

Java内存模型抽象的抽象结构示意图.png

从上图可以看出,如果线程A和线程B要通信的话,必须经过下面两个步骤。

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。

JAVA内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。

Happens-Before一些基本原则如下:

  • 程序顺序原则:一个线程内保证语义的串行性
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,则A必然先于C
  • volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt)先于被中断线程的代码
  • 对象的构造函数的执行、结束先于finalize()方法

下面我们来介绍一下volatile和volatile上面所说的Happens-Before规则。

volatile关键字

为了在合适的场合,确保线程之间的有序性、可见性、原子性,java使用了一些特殊的操作或者关键字来声明,volidate就是其中之一的关键字,另外还有synchronized,final等关键字。

  • 特性
    • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

volatile写-读建立的happens-before关系

volatile对内存可见性影响比它自身的特性更为重要。从JSR-133(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。

volatile的示例代码:


public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    
    public void writer(){
        a = 1; //1
        flag = true; //2
    }
    
    public void reader(){
        if(flag){ //3
            int i = a; //4
            
        }
    }
}
复制代码

假设线程A执行writer()方法之后,线程B执行reader()方法,这个Happens-Before关系可以分为3类:

1)程序次序规则::1 happens-before 2, 3 happens-before 4

2)volatile规则:2 happens-before 3

3)传递性规则: 1 happens-before 4

关系图如下:

volatile happens-before.png

我们可以看到A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

volatile写-读的内存语义

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

volatile读内存语义:当读一个volatile变量时,JMM会把线程对应的本地内存置为无效。线程接下来将从主内存读取中读取共享变量。

volatile内存语义的实现

实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序。

volatile重排序规则表如下图所示:

屏幕快照 2021-08-15 下午2.56.19.png

为了实现volatile的内存语义,编辑器会在生成字节码的时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

在JSR-133之前的旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义。JSR-133增强了volitale的内存语义:严格限制编译器和处理器对volatile变量和普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义

volatile的使用

vaolatile变量很方便,但也存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志。也可以用于表示其它状态的信息,但使用需要非常小心,例如它无法保证一些复合操作(例如i++)的原子性(原子变量提供了“读-改-写”的原子操作,这种情况可以使用原子变量,能确保只有一个线程对变量执行写操作)。

当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

总结

关键字volatile并不能代替锁,仅仅保证对单个volatile 变量的读/写具有原子性,锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。锁从功能上看更加强大。可以说volatile提供了一种比锁更轻量级的线程通信机制,在可伸缩性和执行性能上更具优势。

参考书籍:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》

文章分类
后端
文章标签