概念
1 volatile变量,用来确保将变量的更新操作通知到其他线程。
2 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
3 volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
特性
假如一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,具备以下特性:
1、保证多线程下的可见性
2、对于单个的共享变量的读/写具有原子性,无法保证类似num++的原子性。
3、禁止进行指令重排序(即保证有序性)。即volatile前面的代码先于后面的代码先执行
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java里面,可以通过volatile关键字来保证一定的“有序性”。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,也就是happens-before 原则。
happens-before 原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
Java内存模型 (Java Memory Model,JMM)
Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单来说:
1 所有变量储存在主内存。
2 每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。
3 线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。
线程,主内存,工作内存的交互关系如图。
和volatile有关的操作为:
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
为什么要使用Volatile
Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
volatile的原子性问题
volatile仅仅保障对其修饰的变量的写操作( 以及读操作 )本身的原子性 ,而这并不表示对 volatile 变量的赋值操作一定具有原子性。例如,如下对volatile 变量 count1的赋值操作并不是原子操作:
count1 = count2 + 1;
如果变量count2也是一个共享变量,那么该赋值操作实际上是一个read-modify-write 操作。其执行过程中其他线程可能已经更新了 count2 的值,因此该操作不具备不可分割性,也就不是原子操作。如果变量count2 是一个局部变量,那么该赋值操作就是一个原子操作。
对volatile变量的赋值操作,其右边表达式中只要涉及共享变量 ( 包括被赋值的 volatile 变量本身 ),那么这个赋值操作就不是原子操作。要保障这样操作的原子性, 仍然需要借助锁。
解决num++操作的原子性问题
针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的。
public class Counter { //使用原子操作类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,通过循环CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
}
实现原理
可见性实现原理
将一个共享变量声明为volatile后,会有以下效应
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
volatile能够保证可见性,那么它是如何实现可见性的呢?以X86处理器为例,在对volatile修饰的变量进行写操作时,通过编译器生成反汇编指令后,会发现会多一条Lock前缀,就是由于这条Lock前缀所实现的可见性。Lock前缀在多核处理器中会引发下面这两件事情:
1Lock指令会将当前处理器缓存行的数据写回到主内存。(ps:每个处理器都有自己的cache缓存,每次缓存中操作的变量都是主内存中变量的拷贝)
2 一个处理器写回主内存的操作会造成其他处理的缓存无效。
禁止指令重排原理
通过内存屏障来实现禁止指令重排。
如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FexTyina-1591706164985)(E:\技术帖子\笔记\基础\图\volatile\内存屏障.png)]
volatile的使用优化
在JDK7的并发包里新增了一个队列集合类LinkedTransferQueue,它在使用volatile变量的时候,会采用一种将字节追加到64字节的方法来提高性能。
追加到64字节能够优化性能原因
在很多处理器中它们的L1、L2、L3缓存的高速缓存行都是64字节宽,不支持填充缓存行,例如,现在有两个不足64字节的变量AB,那么在AB变量写入缓存行时会将AB变量的部分数据一起写入一个缓存行中,那么在CPU1和CPU2想同时访问AB变量时是无法实现的,也就是想同时访问一个缓存行的时候会引起冲突,如果可以填充到64字节,AB两个变量会分别写入到两个缓存行中,这样就可以并发,同时进行变量访问,从而提高效率。
总结
volatile是一种轻量级的同步机制,它主要有三个特性:
一是保证共享变量对所有线程的可见性
二是禁止指令重排序优化
三是volatile对于单个的共享变量的读/写具有原子性,无法保证类似num++的原子性,需要通过循环CAS的方式来保证num++操作的原子性。