1. volatile作用
- 在Java的內(nèi)存模型下,線程可以把變量保存在本地內(nèi)存中, 而不是直接在主存中進行讀寫,這就可能造成一個線程在主存中修改變量的值,但另外一個線程還繼續(xù)使用它自己的本地內(nèi)存中變量的值,造成數(shù)據(jù)不一致。要解決這個問題,可以把變量聲明為
volatile,這就指示JVM當每次使用該變量時都得到主存中進行讀取,volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內(nèi)存中重讀該變量的值,而且,當成員變量值發(fā)生變化時,強迫線程將變化值寫回到共享內(nèi)存, 從而保證在任何時刻,不同的線程總是看到該成員變量的同一個值
在對
volatile變量進行寫操作之前的所有可見的共享變量的寫操作,在另一個線程B讀這個volatile變量后,都會馬上對另一個線程B可見
- 為了獲得更好的性能,JVM可能會對指令進行重排序,使用
volitile會禁止重排序,不過這也在一定程度上降低了代碼執(zhí)行效率
2. volatile的正確使用
要使volatile變量提供理想的線程安全,必須同時滿足下面兩個條件:
- 對變量的寫操作不依賴當前值
- 該變量沒有包含在具有其他變量的不變式中
第一個條件使volatile變量不能用作線程安全計數(shù)器, i++看上去是一個單獨的操作,實際上是由讀取-修改-寫入三個操作序列組成的組合操作,必須以原子方式執(zhí)行,但volatile并不提供必須的原子性(如果是單線程操作,則可以忽略第一個條件)
假設現(xiàn)在有一個不變式:min < max
private int min, max;
public void setMin(int value) {
if (value > max) {
throw new IllegalArgumentException();
}
min = value;
}
public void setMax(int max) {
if (value < min) {
throw new IllegalArgumentException();
}
max = value;
}
如果將min和max都聲明為volatile并不足以保證線程安全,假設同一時間,線程A調(diào)用setMin(5), 線程B調(diào)用setMax(3), 由于volitile并不阻塞線程,則兩個操作都會正常進行,但最后min = 5, max = 3,這就會不符合條件
3. 正確使用volatile的模式
1. 狀態(tài)標志
```
volatile boolean shudownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
...
}
}
```
這種類型的狀態(tài)標記的一個公共特性是:通常只有一種狀態(tài)轉(zhuǎn)換,如從false變?yōu)閠rue,這種模式可以擴展到來回轉(zhuǎn)換的狀態(tài)標志,但是只有在轉(zhuǎn)換周期不被察覺的情況下才能擴展(false->true, true->false), 此外,還需要某些原子狀態(tài)轉(zhuǎn)換機制,如原子變量
2. 一次性安全發(fā)布
```
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
```
如果 theFlooble 引用不是 volatile 類型,doWork() 中的代碼在解除對 theFlooble 的引用時,將會得到一個不完全構造的 Flooble
該模式的一個必要條件是:被發(fā)布的對象必須是線程安全的,或者是有效的不可變對象(有效不可變意味著對象的狀態(tài)在發(fā)布之后永遠不會被修改)。volatile 類型的引用可以確保對象的發(fā)布形式的可見性,但是如果對象的狀態(tài)在發(fā)布后將發(fā)生更改,那么就需要額外的同步
3. 獨立觀察
設有一種環(huán)境傳感器能夠感覺環(huán)境溫度。一個后臺線程可能會每隔幾秒讀取一次該傳感器,并更新包含當前文檔的 volatile 變量。然后,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值;使用該模式的另一種應用程序就是收集程序的統(tǒng)計信息
4. volatile bean模式
在volatile bean模式中, JavaBean的所有數(shù)據(jù)成員都是volatile類型, 并且getter和setter方法除了獲取或設置屬性外,不能包含任何邏輯, 此外,對于對象引用的成員, 引用的對象必須是有效不可變的(這將禁止具有數(shù)組值得屬性, 因為當數(shù)組引用被聲明為 volatile 時,只有引用而不是數(shù)組本身具有 volatile語義)
5. 開銷較低的讀-寫鎖策略
如果讀操作遠遠超過寫操作,可以結合使用內(nèi)部鎖和 volatile 變量來減少公共代碼路徑的開銷
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
寫操作違反了使用 volatile 的第一個條件,因此不能使用 volatile 安全地實現(xiàn)計數(shù)器 —— 必須使用鎖,其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執(zhí)行讀操作,因此當使用 volatile 保證讀代碼路徑時,要比使用鎖執(zhí)行全部代碼路徑獲得更高的共享度 —— 就像讀-寫操作一樣。然而,要隨時牢記這種模式的弱點:如果超越了該模式的最基本應用,結合這兩個競爭的同步機制將變得非常困難