Android 開發必知必會:Java 併發之三大性質、synchronized、volatile
theme: smartblue highlight: androidstudio
小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。
本文同時參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金
三大性質:原子性、有序性、可見性
併發程式設計中討論執行緒安全問題繞不開三大性質:原子性、有序性、可見性
原子性
原子(atomic) 本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation) 意為“不可被中斷的一個或一系列操作”。原子性則可以表示為:一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗,有著“同生共死”的感覺。
有序性
指的是在程式碼順序結構中,我們可以直觀的指定程式碼的執行順序, 即從上到下按序執行。但編譯器和CPU處理器會根據自己的決策,對程式碼的執行順序進行重新排序。優化指令的執行順序,提升程式的效能和執行速度,使語句執行順序發生改變,出現重排序,但最終結果看起來沒什麼變化(單核)。
有序性問題
指的是在多執行緒環境下(多核),由於執行語句重排序後,重排序的這一部分沒有一起執行完,就切換到了其它執行緒,導致的結果與預期不符的問題。這就是編譯器的編譯優化給併發程式設計帶來的程式有序性問題
指令重排序
為了使處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入的程式碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果重組,並確保這一結果和順序執行結果是一致的,但是這個過程並不保證各個語句計算的先後順序和輸入程式碼中的順序一致。這就是指令重排序。
可見性
當一個執行緒修改一個執行緒共享變數時,另外的執行緒能夠讀到這個修改的值。也就是說,被修飾的共享變數被任何執行緒讀取的時候都能拿到最新的值。
synchronized
定義:
在多執行緒的環境下,多個執行緒同時訪問共享資源會出現一些問題,而 synchronized 關鍵字則是用來保證執行緒同步的。synchronized 是 Java 提供的一個併發控制的關鍵字。主要有兩種用法,分別是同步方法和同步程式碼塊。也就是說,synchronized 既可以修飾方法也可以修飾程式碼塊。
Java 中的每一個物件都可以作為鎖,這個物件也被稱為 監視器(monitor) 。具體表現為以下3種形式:
- 對於普通同步方法,鎖是當前例項物件。
- 對於靜態同步方法,瑣是當前類的 Class 物件。
- 對於同步方法塊,鎖是 Syschonized 括號裡配置的物件。
作用:
給修飾的方法和程式碼塊加鎖,保證同時只能有一個執行緒訪問。
特點:
有序性、原子性、可見性
使用:
Java:
```java public class SynchronizedTest {
private final User user = new User();
/* * 同步方法,監視器為當前物件 * 此處的鎖和 synchronized(this) 是同樣的 / public synchronized void synchronizedMethod() {}
/* * 同步靜態方法,監視器為當前類的 Class 物件 * 此處的鎖和 synchronized(SynchronizedTest.class) 是同樣的 / public synchronized static void synchronizedStaticMethod() {}
/* * 同步程式碼塊,監視器為 synchronized(object) 傳入的物件 / public void synchronizedCodeBlock() {
/ 監視器為 user 物件,同時只能有一個執行緒拿到 user 鎖 / synchronized (user) { System.out.println(user.name); }
/ 監視器為當前類的例項物件,同時只能有一個執行緒拿到該類的例項鎖 / synchronized (this) { System.out.println("SynchronizedTest"); }
/ 監視器為當前類的 Class 物件,同時只能有一個執行緒拿到當前類的 Class 物件鎖 / synchronized (SynchronizedTest.class) { System.out.println("SynchronizedTest.class"); } } } ```
Kotlin:
```kotlin class SynchronizedTestKt {
private val user = User()
/**
* 同步方法,監視器為當前物件
* 此處的鎖和 synchronized(this) 是同樣的
*/
@Synchronized
fun synchronizedMethod() {}
/**
* 同步程式碼塊,監視器為 synchronized(object) 傳入的物件
*/
fun synchronizedCodeBlock() {
/* 監視器為 user 物件,同時只能有一個執行緒拿到 user 鎖 */
synchronized(user) { println(user.name) }
/* 監視器為當前類的例項物件,同時只能有一個執行緒拿到該類的例項鎖 */
synchronized(this) { println("SynchronizedTestKt") }
/* 監視器為當前類的 Class 物件,同時只能有一個執行緒拿到當前類的 Class 物件鎖 */
synchronized(SynchronizedTestKt::class.java) { println("SynchronizedTestKt.class") }
}
/**
* 伴生物件
*/
companion object {
/**
* 同步伴生物件方法,監視器為當前類的 Class 物件
* 此處的鎖和 synchronized(SynchronizedTestKt::class.java) 是同樣的
*/
@Synchronized
fun synchronizedStaticMethod() {}
}
} ```
volatile
定義:
Java 語言規範第3版中對 volatile 的定義如下:Java 程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應該確保通過排他鎖單獨獲得這個變數。
Java 語言提供了 volatile,在某些情況下比鎖要更加方便。如果一個欄位被宣告成 volatile,Java 執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的 。volatile 是一種輕量且在有限的條件下執行緒安全技術,它保證修飾的變數的可見性和有序性,但非原子性。相對於 synchronize 高效,而常常跟 synchronize 配合使用。
作用:
- 保證了不同執行緒對該變數操作的記憶體可見性
- 禁止指令重排序
特點:
有序性、非原子性、可見性
實現原理:
引《Java 併發程式設計的藝術》書中的例子:
在 X86 處理器下通過工具獲取 JIT 編譯器生成的彙編指令來檢視對 volatile 進行寫操作時,CPU 會做什麼事。
Java 程式碼:
java
instance = new Singleton; // instance 是被 volatile 修飾的變數
轉為彙編:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有 volatile 變數修飾的共享變數進行寫操作的時候會多出第二行彙編程式碼,通過查 IA-32架構軟體開發者手冊可知,Lock 字首的指令在多核處理器下會引發了兩件事情:
- 將當前處理器快取行的資料寫回到系統記憶體。
- 這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。
為了提高處理速度,處理器不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完不知道何時會寫到記憶體。如果對聲明瞭 volatile 的變數進行寫操作,JVM 就會向處理器傳送一條 Lock 字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。
volatile 的兩條實現原則:
- Lock 字首指令會引起處理器快取回寫到記憶體。
- 一個處理器的快取回寫到記憶體會導致其他處理器的快取無效。
使用:
Java:
```java public class Main {
private volatile int variable = 0; } ```
Kotlin:
```kotlin class Main {
@Volatile private var variable: Int = 0 } ```
參考:
《Java 併發程式設計的藝術》