java volatile关键字
因为在很多代码里总是看到这个关键字,但又没有具体的了解下,所以在这里梳理一下。
首先看一个经典例子
可见性例子:
package com.study.juc.testvolatile;
/**
* Created by yangqj on 2017/8/26.
*/
public class TestVolatile {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
new Thread(threadTest).start();
while (true){
if(threadTest.isFlag()){
System.out.println("flag的值被修改了");
break;
}
}
}
}
class ThreadTest implements Runnable{
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag="+isFlag());
}
public boolean isFlag() {
return flag;
}
}
代码很简单,就是主线程和子线程共同访问同一个变量flag,子线程修改了flag的值,然后主线程循环判断flag的值修改了,就break 结束了。
正常逻辑下应该是期望输出:
flag的值被修改了
flag=true
可是运行后发现结果为:
flag=true
可以看到子线程明明修改了flag的值,但主线程还是并没有输出“flag的值被修改了”主线程也一直未结束,进入了死循环。
此时我们在给flag再上volatile关键字:
private volatile boolean flag = false;
然后再运行后发现程序输出正常结果。为什么呢?
说明
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。(以上文字拷贝网上)
Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
总结以上,得出有两块内存,一个是主存,一个是CPU自己的告诉缓存,也就是工作内存。我们画个图
我们上面做的异常的例子的主要原因就是这个工作内存和主内存的值不一致导致的。
整个流程为:
1.主线程读取主内存的flag为false,主线程的工作内存的flag为false。逻辑一直执行while循环
2.子线程修改子线程的工作内存中的flag为true,并且将主内存的flag设置为true。(特地将子线程睡眠300就是为了让子线程在主线程读取了falg为false后再执行,为了放大问题)
3.主线程因为while循环的高效,并没有时间去读取主内存的flag,一直读取工作内存中的flag,所以一直读取的值为false。所以一直循环下去,导致此现象。
而volatile关键字有一个特性就是保证了实例变量在多个线程之间的可见性。通过volatile关键字,强制的从主内存中读取变量的值。执行逻辑如图:
如图所示,线程主题读取的直接是主内存,所以主线程读取到的值肯定是最新的,所以加了volatile之后,正常正常运行。但也因为放弃了工作内存,执行效率上会有一定的下降。
这个现象,主线程没有第一时间读取到最新的flag值的问题也被称为内存的可见性。 当多个线程操作共享数据时,彼此不可见。
原子性例子
volatile关键字有一个致命的缺点就是不支持原子性
请看例子:
package com.study.juc.testvolatile;
import com.thread.PCOne2Many.Run;
/**
* Created by yangqj on 2017/8/26.
*/
public class TestAtomic {
public static void main(String[] args) {
AtomicDemo atimoc = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atimoc).start();
}
}
}
class AtomicDemo implements Runnable{
private int count =0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getCount());
}
public int getCount(){
return count++;
}
}
程序逻辑也是很简单:
主线程开启了10个子线程,10个子线程都去读取一遍AtimocDemo的count值,然后+1.
正常情况下,应该是输出0-9
然而运行结果如下:
0
2
4
0
5
1
0
3
6
7
因为count++的操作并不是原子的
1.需要从内存中读取count值
2.把count加1
3.再更新内存中的count值
从结果中看到有3个0输出,也就是说
1.当1线程读取到count值为0 ,打印为0,并+1后更新主存
2.在1线程未更新主存之前,有另外两个线程2和3线程也读取了原先主存中count的值为0,并且打印了。所以此时3个线程都将主内存的count值设置为1。
这就是原子性,而volatile 关键字不具有互斥性,多个线程可以同时访问,所以也就不能保证变量的原子性
如果要保证其原子性,可以使用相较于synchronized,保证了互斥性,也就保证了原子性。也可以使用java.util.concurrent.atomic包下的常用原子变量。
最后将关键字volatile和synchronized比较一下:
1.关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且vilatile只能修饰于变量,而synchronized可以修饰方法,已经代码块。
2.多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
3.volatile能保证数据的可见性,但不能保证数据的原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将工作内存和主内存的数据做同步。
4.volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。