本篇内容包括:volatile 关键字简介、volatile 保证可见性(包括:关乎不可见性问题描述、JMM内存模型和不可见性的解决方案)以及 volatile 其他特性(包括:volatile 不保证原子性、volatile 原子性的保证操作、volatile 禁止指令重排、内存屏障和 happens-before 规则)
Java 中的 volatile 关键字,用来修饰会被不同线程访问和修改的变量,通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制。
volatile 关键字可以保证 JMM(Java内存模型)三个特征中的两个,即可见性与有序性。
volatile 关键字的两个作用:
首先要明确一点的是:只要直接采用了多线程的并发模型,并采用共享内存的方式作为数据的通讯方式,就一定有可见性问题。
在多线程并发执行下,多线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值的情况。即多线程下修改共享变量会出现变量修改值后的不可见性。
从硬件层面上来讲,当 CPU 需要从主内存获取数据时,会拷贝一份到高速缓存中,CPU 计算时就可以直接在高速缓存中进行数据的读取和写入,提高吞吐量。当数据运行完成后,再将高速缓存的内容刷新到主内存中,此时其他 CPU 看到的才是执行之后的结果,但在这之间存在着时间差。
JMM(Java Memory Model):Java内存模型,是Java虚拟机规范中锁定义的一种内存模型,屏蔽掉了底层不同计算机的区别;
JMM 描述了Java程序中各种变量(线程共享变量)的访问规则,以及JVM中将变量存储到内存和从内存中读取变量这样的底层细节,JMM有以下规定:
本地内存和主内存的关系:
从 JSR-133 开始(即从Jdk5开始),volatile 变量的写-读可以实现线程之间的通信。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
即:volatile 会插入内存屏障以阻止重排序。
下面我们分别在 synchronized 和 volatile 的角度,去体验一下他们对于不可见性的解决方案的工作逻辑。
原子性是指在一次操作或者多次操作中,要么所有的操作前部都得到了执行并且不会受到人格因素的干扰而中断,要么所有的操作都不执行,volttile 不保证原子性
以 COUNT++ 问题为例,操作包含三个步骤:
由此可见,count++ 操作不是一个原子操作,也就是说在某一时刻对某一个值的操作的执行,有可能被其他线程打乱
重排序:为了提高性能,编译器和出常常会对既定的代码执行顺序进行指令重排序
原因:一个好的内存模型实际上会放松对处理器和编译器的规则束缚,也就是说软件技术和硬件技术都为一个目标服务:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,十七能后发挥自身优势,因此,在执行程序时,为提高性能,编译器和处理器常常会对指令集逆行重排序,一般重排序科一分为如下三种:
重排序的好处:重排序可以提高程序处理速度
重排序的问题:单线程的重排序很简单,因为可以通过语义分析就能知道前后代码的依赖性,但是多线程就不一样了,多线程环境里编译器和CPU指令优化根本无法识别多个线程之间存在的数据依赖性。
volatile 解决不可见问题的方式就是插入内存屏障以阻止重排序:
内存屏障分为两种:Load Barrier 和 Store Barrier 即读屏障和写屏障。内存屏障有两个作用:
对于 Load Barrier 来说,在指令前插入 Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于 Store Barrier 来说,在指令后插入 Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
由于内存屏障的作用,避免了 volatile 变量和其它指令重排序、线程之间实现了通信,使得 volatile表 现出了锁的特性。
volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
使用 happens-before 概念阐述了两个操作之间的内存可见性
happens-before 关系定义如下:
happens-before 规则如下:
程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。
volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
线程启动(start) 规则:如果 线程A 执行操作 ThreadB.start()(启动线程B),那么 线程A 的 ThreadB.start() 操作 happens-before 于 线程B 中的任意操作
线程终结(join)规则:如果 线程A 执行操作 ThreadB.join() 并成功返回,那么 线程B 中的任意操作 happens-before 于 线程A 从 ThreadB.join() 操作成功返回。
线程中断操作:对线程 interrupt() 方法的调用,happens-before 于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到线程是否有中断发生。
对象终结规则:一个对象的初始化完成,happens-before 于这个对象的 finalize() 方法的开始。
happens-before 规则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,当我们了解并可以合理的运用 happens-before 规则后,就可以更好的写出线程安全的代码啦!