volatile关键字是什么
在并发编程中synchronized是阻塞式同步,在线程竞争激烈的时候,它会升级为重量级锁,而volatile是Java虚拟机提供的最轻量级的同步机制。
在Java内存模型当中,告诉我们,各个线程会将共享变量从主存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
那在线程的工作内存进行操作后何时会写的主存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量Java虚拟机给予了特殊的约定。
volatile 这个变量具备两种特征:
1. 一种是保证该变量对所有的线程可见的,在一个线程修改了变量值之后,新的值对于其他线程是可以理解获取的。
2. 一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
3. 但volatile不能保证对变量操作的原子性。
volatile实现可见性和有序性
因为在访问volatile 修饰的变量的时候,是不会执行加锁操作的,所以也意味着不会执行线程阻塞,因此volatile变量修饰的是一种比synchronized关键字更轻量级的同步机制,volatile 主要是适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取操作。
在有很多线程对普通变量进行读写的时候,每个线程都首先需要将数据从内存中复制到CPU的缓存当中,如果计算机中有多个CPU,则线程可能都在不同的CPU中执行,这就意味着每个线程都需要将同一个数据复制到不同的CPU cache当中,这样在每个线程对同一个变量的数据做了不同的处理后就可能存在数据不一致的情况。
如果将变量声明了volatile,那么JVM就能保证每次读取变量时能直接从内存中读取,跳过CPU Cache这一步,有效的解决了多线程数据同步的问题。
下面我们就用代码演示一下volatile关键字修饰的可见性。
所以我们在这稍作修改,给我们需要读的变量添加上volatile变量,将它变为在其他线程是可读的。
代码:
public class ThreadDemo implements Runnable{
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;
System.out.println(this.flag);
}
public boolean getFlag() {
return flag;
}
}
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread t = new Thread(td);
t.start();
while(true){
if(td.getFlag()){
System.out.println("main---------------");
break;
}
}
}
}
我们再用一段代码演示一下volatile修饰的有序性。
执行下面的代码,在按照我们代码书写的方式会一直一直不停的循环下去。
public class ZhiLingChongPai {
private static int x;
private static int y;
private static int a;
private static int b;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
但是在这我们只要给前面四个变量加上volatile关键字,就不会再出现任何问题的,哪怕一直跑下去,也是不会出现任何问题的,防止了指令重排的问题,就是确保了有序性。
volatile不能确保原子性
代码演示一下:
public class Atomic {
private volatile static int num=0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+(num++));
}
}.start();
}
}
}
总结
volatile关键字是可以严格保证变量的单词读,写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次的操作。
volatile在某些场景下是可以代替synchronized,但是volatile并不能完全代替synchronized的位置,只有在一些特殊的场景下才会适合使用volatile。
比如:必须在同时满足下面两个条件才能保证并发环境的安全性:
- 对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值操作(Boolean flag = true;)
- 该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile。
而volatile关键字的使用也比较简单,直接在自己需要定义的变量前面加上volatile关键字即可。
volatile boolean falg = true;
volatile底层实现原理
在volatile底层使用的是Memory Barrier(内存屏障)。
内存屏障是一条指令。该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如:一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。
volatile变量编译为汇编指令会多出一个#Lock的前缀。
底层有序性的实现: 主要通过对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。
底层可见性的实现: 主要通过Lock前缀指令 + MESI缓存一致性协议来实现的。对volatile修饰的变量执行写操作时 ,JVM会发送一个Lock前缀指令给CPU,CPU在执行完写操作后,会立即将新值刷新到内存中,同时也因为MESI缓存一致性的协议,其他各个CPU都会对总线嗅探,看自己本地缓存中的数据是否被人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个CPU里的线程在读取该变量时,就会从内存里加载最新的值了,这样就保证了可见性。
● 在每个volatile 写操作 的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,保证了volatile写与之前的写操作指令不会重排序,写完数据之后立即执行flush处理器缓存操作将所有写操作刷到内存,对所有处理器可见。
● 在每个volatile 读操作的前面插入一个LoadLoad屏障,保证了在该变量读操作的时候,如果期末处理器修改了,就必须从其他处理器的Cache或者主内存中加载到自己的本地高速缓存当中,保证读取到的值是最新的。然后在该变量读出走后面插入一个LoadStore屏障,禁止volatile读操作与后面任意读写操作重排序。 屏障类型
屏障类型 | 重排序问题 | 说明 |
---|
LoadLoad | 一个处理器先执行L1读操作,再执行L2度操作,但其他处理器看到的是先L2后L1 | 确保L1的数据读先与L2 | StoreStore | 一个处理器先执行W1写操作,再执行W2写操作,但其他处理器看到的是先W2后W1 | 确保W1对数据的写操作对其他处理器可见(刷到内存)先于W2 | LoadStore | 一个处理器先执行L1读操作,再执行W2写操作,但其他处理器看到的是先W2后L1 | 确保了L1数据的读时先去W2的写操作(刷到内存) | StoreLoad | 一个处理器先执行W1写操作,再执行L2读操作,但其他处理器看到的是先L2后W1 | 确保了W1对数据的写操作对其他处理器可见(刷到内存)先于L2对内存读取的读 |
上一篇:===》详说Java内存模型(JMM)
|