java volatile
ddatsh
锁的两种特性
-
互斥性
一次只允许一个线程持有锁,保证共享数据内容的一致性
-
可见性
确保释放锁之前对共享数据的更改对于随后获得该锁的另一个线程是可见的
Java 两种锁机制:synchronized 和 Lock
为什么要使用Volatile
Volatile变量修饰符如果使用恰当,它比synchronized的使用和执行成本会低,因为它不会引起线程上下文的切换和调度
JVM 内存模型
JDK 1.2前Java内存模型实现只从主存(共享内存)读取变量
但当前(jvm迭代,优化后)内存模型下,线程可以把变量保存在本地内存(TLS)中,而不是直接在主存中进行读写
可能造成一个线程在主存中修改了变量的值,另一个线程还继续使用它在TLS值的拷贝,造成数据的不一致(缓存不一致性问题)
缓存不一致性问题
i = i + 1;
CPU > CPU 高速缓存 > 主存(物理内存)
多核CPU,2线程执行这段代码
初始i=0
两个线程分别读 i 存入各自CPU高速缓存
线程1 加1 操作,i 最新值 1 写入 内存
此时线程2 高速缓存依旧 i=0,加1操作后,i=1,然后线程2 把 i 写入内存
最终 i=1 not 2 > 著名 缓存一致性问题
被多个线程访问的变量 > 共享变量
硬件层解决办法
-
总线加LOCK#锁,期间其他CPU 无法访问内存,效率低下
-
缓存一致性协议
缓存一致性协议
保证每个缓存中共享变量的副本是一致的
CPU写数据时,如操作的变量是共享变量,发信号通知其他CPU将该变量的缓存行置为无效状态,其他CPU读取这个变量时,发现缓存行无效,从内存重新读取
并发三大问题
- 原子性
- 可见性
- 有序性
Java volatile 可看成“轻量 synchronized”,实现 synchronized 的一部分
原子性
如果 32位变量赋值不具备原子性(拆成低16位赋值,高16位赋值)
低16位数值写入之后,线程时间片到了,被中断,此时又一个线程读取 i,去操作,数据不完整
可见
//线程1,CPU1
int i = 0;
i = 10; //加载到高速缓存,但还没写到主存
//线程2,CPU2
j = i; //从主存加载i,线程1的i=10还没刷回主存
有序
指令重排
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
可能2先执行
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
如要重排,只能是 2,1,3,4顺序,不会是2,1,4,3
多线程指令重排
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
1和2没有数据依赖性,假如发生重排序,线程1中先执行语句2,线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,初始化不完全,可能导致程序出错
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性
只要有一个没有被保证,就有可能会导致程序运行不正确
java内存模型,也会存在缓存一致性问题和指令重排序的问题
32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性
但最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了
synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,自然就不存在原子性问题了,从而三个特性都保证了
happens-before原则
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
《深入理解Java虚拟机》
前4条比较重要,后4条显而易见
volatile关键字两层语义
-
保证可见性(一个线程修改变量值后,对其他线程立即可见)
-
禁止指令重排
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
线程1运行的时候,将stop变量拷贝在TLS
线程2更改了stop值后,还没来得及写入主存当中,线程1不知道线程2对stop变量的更改,还一直循环下去
用volatile修饰之后就变得不一样了
volatile不保证原子性
例子1
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
不要在ide里跑
inc进行自增操作,由于volatile保证了可见性,每个线程中inc自增完之后,其他线程中都能看到修改后的值,10线程分别进行1000次操作,最终inc的值应该 1000*10=10000 ?
但自增操作不具备原子性,包括读取变量的原始值、进行加1操作、写入工作内存
自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
某个时刻inc=10,
线程1自增操作先读inc原始值,然后线程被阻塞
然后线程2也进行自增,也去读inc原始值=10(线程1读取后还没有修改,不会导致线程2的工作内存中缓存变量inc缓存行无效,所以线程2会直接去主存读取inc的值)
线程2 inc还=10,然后加1操作,把11写入工作内存,最后写入主存
然后线程1 加1(已读inc工作内存值仍=10),线程1对inc加1后inc值也=11,然后11写入工作内存,最后写入主存
两个线程分别进行了一次自增操作后,inc只增加了1
happens-before规则中的volatile变量规则:修改volatile变量时,会让缓存行无效,然后其他线程去读就会读到新的值
但如果线程1读取后被阻塞了,并没有对inc修改,线程2根本就不会看到修改的值
例子2
public class VolatileTest {
private static volatile long val = 0;
private static class LoopVolatile implements Runnable {
public void run() {
long i = 0;
while (i < 10000000L) {
VolatileTest.val++;
i++;
}
}
}
private static class LoopVolatile2 implements Runnable {
public void run() {
long i = 0;
while (i < 10000000L) {
VolatileTest.val++;
i++;
}
}
}
public static void main(String[] args) {
for (int i=0;i<3;i++){
Thread t1 = new Thread(new LoopVolatile());
t1.start();
Thread t2 = new Thread(new LoopVolatile2());
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println(val);
}
}
}
解法
- synchronized
- Lock
- AtomicInteger/LongAdder(jdk8)
synchronized
public class Test {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
Lock
public class Test {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
AtomicInteger
public class Test {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
volatile 可保证有序性
//线程1:
context = loadContext(); //语句1
volatile inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
volatile 修饰inited 禁止语句1,2 重排,执行到语句2时,必定能保证context已经初始化完毕
volatile 原理和实现机制
先介绍 内存屏障(memory barrier)
一个CPU指令
a) 确保一些特定操作执行的顺序 b) 影响一些数据的可见性(可能是某些指令执行后的结果)
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化
插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行
内存屏障另一个作用是强制更新一次不同CPU的cache line
Java代码:
volatile Singleton instance = new Singleton();//instance是volatile变量
汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
volatile生成的汇编代码,多一个lock前缀指令,形成内存屏障,提供3个功能:
-
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
-
强制将对缓存的修改操作立即写入主存
-
写操作会导致其他CPU中对应的缓存行无效
现代CPU,如访问的内存已在缓存,只lock缓存,提升性能
用缓存一致性机制确保修改的原子性,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据
volatile 使用场景
volatile 无法保证操作原子性,通常必须具备以下2个条件:
1、对变量的写操作不依赖于当前值
不能用作线程安全计数器,x++要取-修改-写入,非原子、单线程写入可忽略
即
不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁),仅仅set或者get的场景是适合volatile的
2、该变量没有包含在具有其他变量的不变式中
即需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行
多个变量之间或者某个变量的当前值与修改后值之间没有约束
单独使用 volatile 不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)
@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
setLower() 和 setUpper() 需要原子化,而字段定义为 volatile 类型无法实现这一目的
合适场景之
状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读
总结
volatile 只能在很小的范围内保证互斥性,坑 > 如果对 volatile 变量本身的操作不是线程安全的(如 ++),是有问题的
volatile 仅仅用来保证该变量对所有线程的可见性,但不保证原子性
具体使用场景参照 Java 理论与实践: 正确使用 Volatile 变量
肤浅的总结起来就是尽量不要用 volatile 来实现互斥
还有个典型的使用 volatile 的场景实在多线程环境下 lazy load 的单例模式,参考 Java 单例真的写对了么?
聊聊并发(一)——深入分析Volatile的实现原理 http://www.infoq.com/cn/articles/ftf-java-volatile#
深入JVM锁机制1-synchronized http://blog.csdn.net/chen77716/article/details/6618779