Java 併發——volatile
JVM記憶體管理
概述
volatile 是輕量級的 synchronized。volatile 作用於共享變數,具備了“鎖”的特性,這是為了確保共享變數能被準確和一致性地更新,這是 volatile 的可見性。同時,它也閹割了 scnchronized 的一寫功能,比如:原子性。
記憶體模型
圖如篇首。
首先我們應該明白CPU是執行命令的場所,當需要處理資料時,CPU會從主記憶體(計算機記憶體)中取值,這樣很慢。後來有了CPU快取記憶體(cache),也就是CPU上有一小塊儲存空間快取了從主記憶體中獲取的資料,CPU直接讀取cache,效率大增。
然後,問題出現了,待處理的資料並非是主記憶體中的原型,而是一個副本,在多執行緒場景下,這個副本的處理結果很有可能會失控。這個問題也就是快取一致性問題,即cache和主記憶體資料同步問題。目前我知道解決快取一致性問題有兩種方案:
通過在匯流排加LOCK鎖
通過快取一致性協議
第一種是通過獨佔CPU方式實現,同一時間只有一個CPU在執行,效率低下。
第二種允許多核處理,並且讓共享副本線上程之間具有可見性。
可見性
通過 volatile 實現了快取一致性,其工作原理如下:
當某個CPU在寫資料時,如果發現操作的變數是共享變數,則會通知其他CPU告知該變數的快取行是無效的,因此其他CPU在讀取該變數時,發現其無效會重新從主存中載入資料。
volatile 的可見性只針對當CPU從主記憶體中載入共享變數的時候。但是當執行緒A、B同時載入了共享變數i,後者說執行緒A先載入了i,在A將i寫入之前,B載入了i,B載入的i仍然是主記憶體中i的初始值。
非原子性
執行緒記憶體與主記憶體的互動過程如下:
主要有以下5項操作:
read:從主記憶體中讀取變數
load:複製變數到執行緒本地記憶體作為副本
use:運算副本
assign:給副本賦值
store:副本寫入執行緒本地記憶體
write:執行緒記憶體中的共享副本重新整理主記憶體中的共享變數
上一章節——“可見性”中已經宣告 volatile 的可見性只針對當CPU從主記憶體中載入共享變數的時候,即load之前,一旦load,無論主記憶體中的共享變數發生了什麼,副本的值不會被主記憶體同步。也就是說,volatile不具有原子性。
網友解答:
volatile的非原子性:執行緒工作內容中的值從主記憶體中直接載入,一旦載入完成,就不會再產生對應的變化。JVM保證的是從主記憶體中載入到執行緒工作記憶體中的值是最新的,但是無法保證原子性。 volatile解決的是變數讀時的可見性問題,無法保證原子性。
package com.zhoupq.multiThread.Volatile;
public class VolatileDemo implements Runnable
{
static volatile int i = 1;br/>@Override
public void run()
{
System.out.println(Thread.currentThread().getName() + ": " + i + ", "
- (++i));
}
public static void main(String[] args)
{
Thread t1 = new Thread(new VolatileDemo(), "A");
Thread t2 = new Thread(new VolatileDemo(), "B");
Thread t3 = new Thread(new VolatileDemo(), "C");
Thread t4 = new Thread(new VolatileDemo(), "D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/
A: 1, 2
B: 1, 3
C: 3, 4
D: 4, 5
/
結果與預期不符,在預期的結果中,執行緒B應該載入的i是2,運算之後變成3。由此我們知道,volatile 不能解決併發計算問題。
網友解答:
- i 和+(++i)是兩條指令,會發生這種情況:
假設當前i為1,執行緒A執行+i,執行緒B執行+i,然後執行緒A執行+(++i),這時i=2,然後執行緒B執行+(++i),這時i=3,執行緒A輸出(1, 2),執行緒B輸出(1, 3)。
小結
volatile 保證可見性
volatile 不保證原子性
volatile 不能解決併發計算問題