Volatile关键字实现原理
@TOC
voliate简介
在上一篇文章中我们深入理解了java关键字synchronized,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。
通过上一篇的文章我们了解到synchronized是阻塞同步的,在线程竞争激烈的情况下会升级为重量级锁。而voliate就可以说是java虚拟机提供的最轻量级的同步锁。但它同时不容易被正确理解,也至于在并发编程中有很多程序员遇到线程安全的问题就会使用sychronized。Java内存模型告诉我们,各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内容中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对于普通变量是没有规定的,而针对voliate修饰的变量给java虚拟机特殊的约定,线程对voliate变量的修改会立即被其他线程感知。
机器硬件CPU与JMM
- CPU Cache模型
为了解决CPU的高速性和内存读取的低效性,在CPU和内存之间加入了Cache缓存,平衡了二者之间的差距,为了更高效更平滑的读取数据,引入了一级缓存、二级缓存、三级缓存。
上图为程序的局部执行原理,其中内存中的数据为共享数据。每一次CPU修改内存中的数据,需要进行如下操作:- 从内存中读取Cache中
- 在Cache中更新数据
- 把更新的数据刷新到内存汇总
上述操作存在CPU缓存不一致的问题。
- 一致性问题解决方案
- 总线加锁(粒度太大)
对数据总线、地址总线、控制总线进行加锁,造成效率低下,加锁粒度太大。 - MESI
- 读操作:不做任何事情,把Cache中的数据读到寄存器
- 写操作:发出信号通知其他的CPU将该变量的Cache line置为无效,其他的CPU要访问这个变量的时候,只能从内存中获取,而不能从Cache中去读。
- CPU的cache中会增加很多的Cache line
- Java内存模型
- 主存中的数据所有线程都可以访问(共享数据)
- 每个线程都有自己的工作空间,(本地内存)(私有数据)
- 工作空间数据:局部变量、内存的副本
- 线程不能直接修改内存中的数据,只能读到工作空间来修改,修改完成后刷新到内存
- 总线加锁(粒度太大)
Volatile关键字的语义分析
volatile作用:让其他线程能够马上感知到某一线程多某个变量的修改
保证可见性
对共享变量的修改,其他的线程马上能感知到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.leehao;
import java.util.concurrent.TimeUnit;
public class ReaderAndUpdater {
final static int MAX=50;
static int init_value=0;
public static void main(String[] args) {
new Thread(()->{
int localValue=0;
while(localValue<MAX){
if(localValue!=init_value){
System.out.println("Reader:"+init_value);
localValue=init_value;
}
}
},"Reader").start();
new Thread(()->{
int localValue=0;
while(localValue<MAX){
System.out.println("updater:"+(++localValue));
init_value=localValue;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Updater").start();
}
}上述程序中,2个线程中的init_value是互相不可见的,所以程序Reader永远无法监听到Updater中的变化,从而输出reader:XXX。
1
2
3
4
5
6
7
8
9
10
11
12updater:1
Reader:1
updater:2
updater:3
updater:4
updater:5
updater:6
updater:7
updater:8
updater:9
updater:10
updater:11如过加入volidate修饰符,那么只要init_value有变化,那么对reader可见,reader对init_value进行监听到,输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18updater:1
Reader:1
updater:2
Reader:2
updater:3
Reader:3
updater:4
Reader:4
updater:5
Reader:5
updater:6
Reader:6
updater:7
Reader:7
updater:8
Reader:8
...
...但不能保证原子性,读、写、(i++)
保证有序性
重排序(编译阶段、指令优化阶段)
输入程序的代码顺序并不是实际执行的顺序
重排序后对单线程没有影响,对多线程有影响
Volatile
Happens-before
volatile规则:
对于volatile修饰的变量:
(1)volatile之前的代码不能调整到他的后面
(2)volatile之后的代码不能调整到他的前面(as if seria)
(3)霸道(位置不变化)1
2
3
4
5
6
7int i=0;
int a=2;
int b=3;
volatile int j=3;
int c=2;
int d=3;
int e=c+d;上述代码限定,在重排序的时候,volatile前面的代码必须在
volatile int j=3;
之前执行,volatile之后的代码必须在volatile int j=3;
之后执行。但前面的代码和后面的代码内部可以进行重排序。volatile的原理和实现机制(锁、轻量级)
HSDIS反编译:
编译顺序Java –class—JVM—ASM文件(会变文件)volatile int a ;
编译成会变文件后:Lock :a
注: synchronize
在1.5之前为重量级锁,1.6之后为偏向锁、轻量级锁。但是偏向锁和轻量级锁需要进行配置,如果不配置,默认还是重量级锁。
Volatile的使用场景
状态标志(开关模式)
started启动默认为false,当started为true的时候,执行一系列的操作。当某个线程在shutdown()中可以将started设置为false,那么所有的线程操作关闭。
伪代码:
1 |
|
双重检查锁定(double-checked-locking)
单例模式
1 |
|
volatile与synchronized的区别
- 使用上的区别
Volatile只能修饰变量,synchronized只能修饰方法和语句块 - 对原子性的保证
synchronized可以保证原子性,volatile不能保证原子性 - 对可见性的保证
都可以保证可见性,但实现原理不同
volatile对变量加了lock,synchronized使用monitorEnter和monitorexit 对应刀JVM中是monitor指令 - 对有序性的保证
volatile能保证有序,synchronizedye 可以保证有序性,但是代价太大,会升级到重量级锁,并发退化到串行 - 其他
synchronized 会引起线程阻塞
volatile 不会引起线程阻塞