Java執行緒安全

千鋒IT教育發表於2022-11-09

執行緒安全的本質

其實第一張圖的例子是有問題的,主記憶體中的變數是共享的,所有執行緒都可以訪問讀寫,而執行緒工作記憶體又是執行緒私有的,執行緒間不可互相訪問。那在多執行緒場景下,圖上的執行緒 A 和執行緒 B 同時來操做共享記憶體裡的同一個變數,那麼主記憶體內的此變數資料就會被破壞。也就是說主記憶體內的此變數不是執行緒安全的。我們來看個程式碼小例子幫助理解。

public class ThreadDemo {
    private int x = 0;
    private void count() {
        x++;
    }
    public void runTest() {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("final x from 1: " + x);
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("final x from 2: " + x);
            }
        }.start();
    }
    public static void main(String[] args) {
        new ThreadDemo().runTest();
    }
}

示例程式碼中 runTest 方法2個執行緒分別執行 1_000_000 次 count() 方法, count() 方法中只執行簡單的 x++ 操作,理論上每次執行 runTest 方法應該有一個執行緒輸出的 x 結果應該是2_000_000。但實際的執行結果並非我們所想:

final x from 1: 989840
final x from 2: 1872479

我執行了10次,其中一個執行緒輸出 x 的值為 2_000_000 只出現了2次。

final x from 1: 1000000
final x from 2: 2000000

出現這樣的結果的原因也就是我們上面所說的,在多執行緒環境下,我們主記憶體的 x 變數的資料被破壞了。我們都知道完成一次 i++ 相當於執行了:

int tmp = x + 1;
x = tmp;

在多執行緒環境下就會出現在執行完 int tmp = x + 1; 這行程式碼時就發生了執行緒切換,當執行緒再次切回來的時候,x 就會被重複賦值,導致出現上面的執行結果,2個執行緒都無法輸出 2_000_000。

下圖描述了示例程式碼的執行時序:



那麼 Java 是如何來解決上述問題來保證執行緒安全,保證共享記憶體的原子性、可見性、有序性的呢?

執行緒同步

Java 提供了一系列的關鍵字和類來保證執行緒安全。

Synchronized 關鍵字

Synchronized 作用

保證方法或程式碼塊操作的原子性

Synchronized 保證⽅法內部或程式碼塊內部資源(資料)的互斥訪問。即同⼀時間、由同⼀個 Monitor(監視鎖) 監視的程式碼,最多隻能有⼀個執行緒在訪問。

話不多說來張動圖描述一下 Monitor 工作機制:


動圖封面



動圖封面


被 Synchronized 關鍵字描述的方法或程式碼塊在多執行緒環境下同一時間只能由一個執行緒進行訪問,在持有當前 Monitor 的執行緒執行完成之前,其他執行緒想要呼叫相關方法就必須進行排隊,知道持有持有當前 Monitor 的執行緒執行結束,釋放 Monitor ,下一個執行緒才可獲取 Monitor 執行。

如果存在多個 Monitor 的情況時,多個 Monitor 之間是不互斥的。

多個 Monitor 的情況出現在自定義多個鎖分別來描述不同的方法或程式碼塊,Synchronized 在描述程式碼塊時可以指定自定義 Monitor ,預設為 this 即當前類。


動圖封面



動圖封面


保證監視資源的可見性

保證多執行緒環境下對監視資源的資料同步。即任何執行緒在獲取到 Monitor 後的第⼀時 間,會先將共享記憶體中的資料複製到⾃⼰的快取中;任何執行緒在釋放 Monitor 的第⼀ 時間,會先將快取中的資料複製到共享記憶體中。

保證執行緒間操作的有序性

Synchronized 的原子性保證了由其描述的方法或程式碼操作具有有序性,同一時間只能由最多隻能有一個執行緒訪問,不會觸發 JMM 指令重排機制。

Volatile 關鍵字

Volatile 作用

保證被 Volatile 關鍵字描述變數的操作具有可見性和有序性(禁止指令重排)。

注意:

Volatile 只對基本型別 (byte、char、short、int、long、float、double、 boolean) 的賦值 操作和物件的引⽤賦值操作有效。對於 i++ 此類複合操作, Volatile 無法保證其有序性和原子性。相對 Synchronized 來說 Volatile 更加輕量一些。

java.util.concurrent.atomic

包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等類。使用這些類來宣告變數可以保證對其操作具有原子性來保證執行緒安全。

實現原理上與 Synchronized 使用 Monitor(監視鎖)保證資源在多執行緒環境下阻塞互斥訪問不同,java.util.concurrent.atomic 包下的各原子類基於 CAS(CompareAndSwap) 操作原理實現。

CAS 又稱無鎖操作,一種樂觀鎖策略,原理就是多執行緒環境下各執行緒訪問共享變數不會加鎖阻塞排隊,執行緒不會被掛起。通俗來講就是一直迴圈對比,如果有訪問衝突則重試,直到沒有衝突為止。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70023145/viewspace-2922608/,如需轉載,請註明出處,否則將追究法律責任。

相關文章