[深入理解Java虛擬機器]原子性/可見性/有序性

Duancf發表於2024-07-26

原子性、可見性與有序性

Java記憶體模型是圍繞著在併發過程中如何處理原子性、可見性和有序性這三個特徵來建立的,我們逐個來看一下哪些操作實現了這三個特性。

  1. 原子性(Atomicity)

由Java記憶體模型來直接保證的原子性變數操作包括read、load、assign、use、store和write這六個, 我們大致可以認為,基本資料型別的訪問、讀寫都是具備原子性的(例外就是long和double的非原子性協定,讀者只要知道這件事情就可以了,無須太過在意這些幾乎不會發生的例外情況)。 如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java記憶體模型還提供了lock和 unlock操作來滿足這種需求,儘管虛擬機器未把lock和unlock操作直接開放給使用者使用,但是卻提供了更 高層次的位元組碼指令monitorenter和monitorexit來隱式地使用這兩個操作。這兩個位元組碼指令反映到Java 程式碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。

  1. 可見性(Visibility)

可見性就是指當一個執行緒修改了共享變數的值時,其他執行緒能夠立即得知這個修改。

上文在講解 volatile變數的時候我們已詳細討論過這一點。Java記憶體模型是透過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的,無論是普通變數還是volatile變數都是如此。普通變數與volatile變數的區別是,volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。因此我們可以說volatile保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。 除了volatile之外,Java還有兩個關鍵字能實現可見性,它們是synchronizedfinal

同步塊的可見性是由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store、write操 作)”這條規則獲得的。

而final關鍵字的可見性是指:被final修飾的欄位在構造器中一旦被初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他執行緒有可能通 過這個引用訪問到“初始化了一半”的物件),那麼在其他執行緒中就能看見final欄位的值。

如程式碼清單 12-7所示,變數i與j都具備可見性,它們無須同步就能被其他執行緒正確訪問。

public static final int i;
public final int j;
static {
	i = 0; // 省略後續動作
}
{
	// 也可以選擇在建構函式中初始化
	j = 0; // 省略後續動作 
}
  1. 有序性(Ordering)

Java記憶體模型的有序性在前面講解volatile時也比較詳細地討論過了,Java程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒, 所有的操作都是無序的。前半句是指執行緒內似表現為序列的語義(Within-Thread As-If-Serial Semantics),後半句是指“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象。 Java語言提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性,volatile關鍵字本 身就包含了禁止指令重排序的語義,而synchronized則是由“一個變數在同一個時刻只允許一條執行緒對 其進行lock操作”這條規則獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能序列地進入。 介紹完併發中三種重要的特性,讀者是否發現synchronized關鍵字在需要這三種特性的時候都可以 作為其中一種的解決方案?看起來很“萬能”吧?的確,絕大部分併發控制操作都能使用synchronized來 完成。synchronized的“萬能”也間接造就了它被程式設計師濫用的局面,越“萬能”的併發控制,通常會伴隨 著越大的效能影響。

相關文章