volatile小记

  • Post author:
  • Post category:其他





前言


并发三大特性:可见性,有序性,原子性。

  • 可见性:简单来说,就是一个线程对共享变量的修改,对其他线程可见。
  • 有序性:指的是程序按照代码编译后的先后顺序执行。
  • 原子性:指的是一个操作是不可中断的,即一个操作一旦开始就不会被其他线程影响

Java关键字

volatile

具有可见性和有序性的特性,针对两种特性的实现,我们来探讨一下实现原理,以及为什么volatile不具有原子性。




一、Java内存模型(JMM)

  • Java内存模型规定所有变量都存储在主内存中
  • 每个线程都有自己的工作(本地)内存,工作内存中保存了变量副本。

    在这里插入图片描述

    线程之间如果需要保证共享变量可见性,必须做到:线程修改后的共享变量值能够及时从工作内存刷新到主内存中,以及其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中。



二、volatile实现

volatile通过内存屏障保证有序性和可见性



内存屏障

硬件层的内存屏障分为两种:Load Barrier(读屏障) 和 Store Barrier(写屏障)。

内存屏障有两个作用:

  • 阻止屏障两侧的指令重排序;
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

JVM内存屏障

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令
StoreStoreBarrier 
volatile写
StoreLoadBarrier 
volatile写
LoadLoadBarrier
LoadStoreBarrier 



有序性

重排序包括:

  • 编译器优化的重排序(编译器优化)
  • 指令级并行的重排序(处理器优化)
  • 内存系统的重排序(处理器优化)


as-if-serial语义

as-if-serial含义指的是无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致


happens-before 原则

从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:

  • 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性规则:A先于B ,B先于C 那么A必然先于C
  • 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则:对象的构造函数执行,结束先于finalize()方法



可见性

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷新CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

  • 将当前处理器缓存行数据立即写回主存
  • 写操作会触发总线嗅探机制(MESI协议)



原子性

Java内存模型中定义的8种工作内存与主内存之间的原子操作

操 作 说明
lock( 锁定 ) 作用于主内存的变量,把一个变量标识为一条线程独占的状态。
unlock(解锁 作用于主内存的变量,把一个处于锁定的变量释放出来,释放变量才可以被其他线程锁定。
read(读取) 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用) 作用于工作内存种的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值) 作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入) 作用于主内存的变量,它把store操作从工作内存中得到的值放入主内存的变量中。

在这里插入图片描述

volatile不能保证变量复合操作的原子性

public class TestA {

    private static volatile int count = 0;

    public static void main (String [] args) {
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();
        }
        System.out.println("count:" + count);
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            count ++;
        }
    }
}

结果

count:560

修改volatile变量分为四步:

  • 读取volatile变量到本地(read/load)
  • 修改变量值(assign)
  • local值写回(store/load)
  • 插入内存屏障,即lock指令,让其他线程可见

如果线程1读取count值为0,然后自增为1,此时并没有写回(store)主存,其他线程对该修改不可见;CPU调度线程2进行类似操作,然后写回主存;CPU调度线程1继续执行,然后将修改后的值1写入主存,导致线程2的更新丢失。因此volatile不具有原子性。



总结


任何带有lock前缀指令都具有内存屏障作用,Lock前缀不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁



版权声明:本文为weixin_38422258原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。