在《Java 并發(fā)編程:核心理論》一文中恰矩,我們已經(jīng)提到可見性、有序性及原子性 問題憎蛤,通常情況下我們可以通過 Synchronized 關(guān)鍵字來解決這些個(gè)問題外傅,不過 如果對(duì) Synchonized 原理有了解的話,應(yīng)該知道 Synchronized 是一個(gè)較重量級(jí) 的操作俩檬,對(duì)系統(tǒng)的性能有比較大的影響萎胰,所以如果有其他解決方案,我們通常都 避免使用 Synchronized 來解決問題棚辽。
而 volatile 關(guān)鍵字就是 Java 中提供的另一種解決可見性有序性問題的方案技竟。對(duì)于 原子性,需要強(qiáng)調(diào)一點(diǎn)屈藐,也是大家容易誤解的一點(diǎn):對(duì) volatile 變量的單次讀/ 寫操作可保證原子性的灵奖,如 long 和 double 類型變量,但是并不能保證 i++這種 操作的原子性估盘,因?yàn)楸举|(zhì)上 i++是讀、寫兩次操作骡尽。 volatile 也是互斥同步的一種實(shí)現(xiàn)遣妥,不過它非常的輕量級(jí)。 volatile 的意義攀细? 線程會(huì)一直等待箫踩。 可以嘗試獲得鎖,線程可以不用一直等待 鎖狀態(tài) 無法判斷 可以判斷 鎖類型 可重入 不可中斷 非公平 可重入 可判斷 可公平(兩者皆可) 性能 少量同步 大量同步
-
防止 CPU 指令重排序 volatile 有兩條關(guān)鍵的語義: 保證被 volatile 修飾的變量對(duì)所有線程都是可見的 禁止進(jìn)行指令重排序 要理解 volatile 關(guān)鍵字谭贪,我們得先從 Java 的線程模型開始說起境钟。如圖所示:
Java 內(nèi)存模型規(guī)定了所有字段(這些字段包括實(shí)例字段、靜態(tài)字段等俭识,不包括局 部變量慨削、方法參數(shù)等,因?yàn)檫@些是線程私有的套媚,并不存在競(jìng)爭(zhēng))都存在主內(nèi)存中缚态, 每個(gè)線程會(huì) 有自己的工作內(nèi)存,工作內(nèi)存里保存了線程所使用到的變量在主內(nèi) 存里的副本拷貝堤瘤,線程對(duì)變量的操作只能在工作內(nèi)存里進(jìn)行玫芦,而不能直接讀寫主 內(nèi)存,當(dāng)然不同內(nèi)存之間也 無法直接訪問對(duì)方的工作內(nèi)存本辐,也就是說主內(nèi)存是 線程傳值的媒介桥帆。
我們來理解第一句話:
保證被 volatile 修飾的變量對(duì)所有線程都是可見的 如何保證可見性医增?
被 volatile 修飾的變量在工作內(nèi)存修改后會(huì)被強(qiáng)制寫回主內(nèi)存,其他線程在使用 時(shí)也會(huì)強(qiáng)制從主內(nèi)存刷新老虫,這樣就保證了一致性叶骨。 關(guān)于“保證被 volatile 修飾的變量對(duì)所有線程都是可見的”,有種常見的錯(cuò)誤理解: - 由于 volatile 修飾的變量在各個(gè)線程里都是一致的张遭,所以基于 volatile 變 量的運(yùn)算在多線程并發(fā)的情況下是安全的邓萨。 這句話的前半部分是對(duì)的,后半部分卻錯(cuò)了菊卷,因此它忘記考慮變量的操作是否具有原子性這一問題缔恳。
舉個(gè)例子:
private volatile int start = 0;
private void volatile Keyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
}; for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable); thread.start();
} Log.d(TAG, "start = " + start);
}
這段代碼啟動(dòng)了 10 個(gè)線程,每次 10 次自增洁闰,按道理最終結(jié)果應(yīng)該是 100歉甚,但是 結(jié)果并非如此。 為什么會(huì)這樣扑眉?
仔細(xì)看一下 start++纸泄,它其實(shí)并非一個(gè)原子操作,簡(jiǎn)單來看腰素,它有兩步:
1聘裁、取出 start 的值,因?yàn)橛?volatile 的修飾弓千,這時(shí)候的值是正確的衡便。
2、自增洋访,但是自增的時(shí)候镣陕,別的線程可能已經(jīng)把 start 加大了,這種情況下就有 可能把較小的 start 寫回主內(nèi)存中姻政。
所以 volatile 只能保證可見性呆抑,在不符合以 下場(chǎng)景下我們依然需要通過加鎖來保證原子性:
- 運(yùn)算結(jié)果并不依賴變量當(dāng)前的值,或者只有單一線程修改變量的值汁展。(要 么結(jié)果不依賴當(dāng)前值鹊碍,要么操作是原子性的,要么只要一個(gè)線程修改變量 的值) - 變量不需要與其他狀態(tài)變量共同參與不變約束 比方說我們會(huì)在線程里加 個(gè) boolean 變量食绿,來判斷線程是否停止妹萨,這種情況就非常適合使用 volatile。
我們?cè)賮砝斫獾诙湓挕?禁止進(jìn)行指令重排序 什么是指令重排序炫欺?
指令重排序是指指令亂序執(zhí)行乎完,即在條件允許的情況下直接運(yùn)行當(dāng)前有能 力立即執(zhí)行的后續(xù)指令,避開為獲取一條指令所需數(shù)據(jù)而造成的等待品洛,通 過亂序執(zhí)行的技術(shù)提供執(zhí)行效率树姨。 指令重排序會(huì)在被 volatile 修飾的變量的賦值操作前摩桶,添加一個(gè)內(nèi)存屏障, 指令重排序時(shí)不能把后面的指令重排序移到內(nèi)存屏障之前的位置帽揪。