volatile的作用歸結(jié)起來有兩點:
1山橄、保證內(nèi)存可見性
2、防止指令重排序
但是要注意一點
保證double,float的讀寫的原子性晌该,但是無法保證復(fù)合操作的原子性
分別解釋:
一肥荔、 保證內(nèi)存可見性
CPU和物理內(nèi)存之間的通信速度遠慢于CPU的處理速度,為了提高處理速度朝群,處理器不直接和內(nèi)存進行通信燕耿,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進行操作姜胖,但操作完不知道何時會寫到主內(nèi)存誉帅。
在多線程的情況下就會出現(xiàn)“緩存一致性”問題。
/**
*
* @author fukailong
*
*/
public class Test {
private boolean flag = false;
public void doSomethind() {
while (!flag) {
// System.out.println("執(zhí)行中..");//這個位置不要打印右莱,因為打印容易觸發(fā)將線程工作內(nèi)存中的變量回刷到主存中
}
System.out.println("結(jié)束了..");
}
public static void main(String[] args) throws Exception {
final Test sharedObject = new Test();
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
sharedObject.doSomethind();
};
}.start();
}
Thread.sleep(100);
new Thread() {
public void run() {
sharedObject.flag = true;
};
}.start();
}
}
大部分情況下線程主函數(shù)會結(jié)束并打印三個“結(jié)束了...”,因為現(xiàn)在的jvm其實已經(jīng)實現(xiàn)了緩存一致性協(xié)議慢蜓,也就是說當(dāng)后面的線程修改了flag共享變量后亚再,系統(tǒng)會盡可能將線程工作內(nèi)存中的變量回刷到主存中。但這只是盡量晨抡,這就造成了某個線程會一直執(zhí)行循環(huán)體氛悬,也就是你看到的打印了兩個“結(jié)束了...”。
如果對聲明了volatile的變量進行寫操作凄诞,JVM就會向處理器發(fā)送一條Lock前綴的指令圆雁,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。一個處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效帆谍,線程接下來將從主內(nèi)存中讀取共享變量伪朽。這樣就可以保證線程能讀取到其他修改的變量值。
增加volatile后效果如下:
/**
*
* @author fukailong
*
*/
public class Test {
private volatile boolean flag = false;
public void doSomethind() {
while (!flag) {
// System.out.println("執(zhí)行中..");//這個位置不要打印汛蝙,因為打印容易觸發(fā)將線程工作內(nèi)存中的變量回刷到主存中
}
System.out.println("結(jié)束了..");
}
public static void main(String[] args) throws Exception {
final Test sharedObject = new Test();
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
sharedObject.doSomethind();
};
}.start();
}
Thread.sleep(100);
new Thread() {
public void run() {
sharedObject.flag = true;
};
}.start();
}
}
二、 防止指令重排序
指令重排序是JVM為了優(yōu)化指令窖剑,提高程序運行效率坚洽,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度西土。編譯器讶舰、處理器也遵循這樣一個目標(biāo)。
在每個volatile寫操作的前面插入一個StoreStore屏障需了。
·在每個volatile寫操作的后面插入一個StoreLoad屏障跳昼。
·在每個volatile讀操作的后面插入一個LoadLoad屏障。
·在每個volatile讀操作的后面插入一個LoadStore屏障肋乍。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getsingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上是懶漢式單例的寫法:
如果不加volatile鹅颊,代碼中 singleton = new Singleton()這句,這并非是一個原子操作墓造,事實上在JVM中這句話大概做了下面3件事情
1.給singleton分配內(nèi)存
2.調(diào)用Singleton的構(gòu)造函數(shù)來初始化成員變量
3.將singleton對象指向分配的內(nèi)存空間(執(zhí)行到這一步堪伍,singleton才是非null的了)
但是在JVM的及時編譯器中锚烦,存在指令重排序的優(yōu)化,也就是說帝雇,第二步和第三步的順序是無法保證的 從而導(dǎo)致程序出錯涮俄。所以需要添加volatile來避免指令重排序。
三摊求、 無法保證原子性
public class Test {
volatile static int a = 0;
private static CountDownLatch countDownLatch;
public static void main(String[] args) throws InterruptedException {
countDownLatch = new CountDownLatch(10000);
for (int i = 0; i < 10000; i++) {
new Thread() {
public void run() {
a++;
countDownLatch.countDown();
}
}.start();
}
countDownLatch.await();
System.out.println(a);
}
}
代碼打印的值不一定是10000禽拔,可能會是小于10000的值。
雖然變量a 設(shè)置了volatile室叉,但是不能保證a++的操作的原子性睹栖,a++包括:讀-寫-改 的復(fù)合操作還是沒有變化。
所以volatile不能保證原子性茧痕,但用volatile修飾long和double可以保證其操作原子性野来。
從Oracle Java Spec里面可以看到:
1.對于64位的long和double,如果沒有被volatile修飾踪旷,那么對其操作可以不是原子的曼氛。在操作的時候,可以分成兩步令野,每次對32位操作舀患。
2.如果使用volatile修飾long和double,那么其讀寫都是原子操作
3.對于64位的引用地址的讀寫气破,都是原子操作
4.在實現(xiàn)JVM時聊浅,可以自由選擇是否把讀寫long和double作為原子操作
我是double,坐標(biāo)魔都现使,QQ群:692595133
如果讀完覺得有收獲的話低匙,歡迎點贊加關(guān)注。
github博客: https://fukailong.github.io
版權(quán)歸作者所有碳锈,轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)顽冶。