Java 多執行緒基礎(四)執行緒安全

學無止境發表於2020-06-11

Java 多執行緒基礎(四)執行緒安全

在多執行緒環境下,如果有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。程式每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。 在瞭解執行緒安全之前,先來說一下Java的記憶體模型 JMM ,先了解多執行緒是如何工作的。

一、JMM(Java Memory Model)

JMM是一種基於計算機記憶體模型(定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範),遮蔽了各種硬體和作業系統的訪問差異的,保證了Java程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。保證共享記憶體的原子性、可見性、有序性。

(一)、多執行緒的執行環境

上圖描述了一個多執行緒執行場景。執行緒 A 和執行緒 B 分別對主記憶體的變數進行讀寫操作。其中主記憶體中的變數為共享變數,也就是說此變數只此一份,多個執行緒間共享。但是執行緒不能直接讀寫主記憶體的共享變數,每個執行緒都有自己的工作記憶體,執行緒需要讀寫主記憶體的共享變數時需要先將該變數拷貝一份副本到自己的工作記憶體,然後在自己的工作記憶體中對該變數進行所有操作,執行緒工作記憶體對變數副本完成操作之後需要將結果同步至主記憶體。

執行緒的工作記憶體是執行緒私有記憶體,執行緒間無法互相訪問對方的工作記憶體。

(二)、多執行緒執行的流程

從上圖可以看出,執行緒A執行完x++後,將資料同步到主記憶體中,執行緒B再訪問時,是執行緒A同步後的資料,所以保證了執行緒安全。那麼,執行緒工作記憶體怎麼知道什麼時候又是怎樣將資料同步到主記憶體呢?這是因為有了 JMM。JMM 規定了何時以及如何做執行緒工作記憶體與主記憶體之間的資料同步。

二、多執行緒中的核心概念

(一)、原子性:對共享記憶體的操作必須是要麼全部執行直到執行結束,且中間過程不能被任何外部因素打斷,要麼就不執行。

(二)、可見性:多執行緒操作共享記憶體時,執行結果能夠及時的同步到共享記憶體,確保其他執行緒對此結果及時可見。

(三)、有序性:程式的執行順序按照程式碼順序執行,在單執行緒環境下,程式的執行都是有序的,但是在多執行緒環境下,JMM 為了效能優化,編譯器和處理器會對指令進行重排,程式的執行會變成無序。

三、執行緒安全

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

public class ThreadDemo {
    private int x = 0;
    public void count() {
        x++;
    }
    public void test() {
        // 執行緒A
        new Thread(() ->{
            for (int i = 0; i < 1000000; i++) {
                count();
            }
            System.out.println(Thread.currentThread().getName() + " : " + x);
        }).start();
        // 執行緒B
        new Thread(() ->{
            for (int i = 0; i < 1000000; i++) {
                count();
            }
            System.out.println(Thread.currentThread().getName() + " : " + x);
        }).start();
        
    }
    public static void main(String[] args) {
        new ThreadDemo().test();
    }
}

 

// 輸出結果
Thread-0 : 1033950
Thread-1 : 1944281

上面程式碼開啟兩個執行緒,分別呼叫 count() 執行 x++ 的操作,理論上,兩個執行緒執行完成後,應該有一個執行緒會輸出 x = 2000000,但是從結果上看,和預期的結果不同,出現這樣的結果的原因也就是我們上面所說的,在多執行緒環境下,我們主記憶體的 x 變數的資料被破壞了。執行流程如下圖:

 

從上圖可以看到,執行緒A和執行緒B共同訪問同一資源 x , 其中執行緒A在對資源進行寫操作中途,執行緒B對這個執行緒A寫了一半的資源進行讀寫操作,導致資源資料 x 出現了錯誤。為了避免出現這種情況,Java 提供了執行緒同步的方法來解決這個問題。執行緒同步內容可以參考:Java 多執行緒基礎(五)執行緒同步

四、總結

(一)、出現執行緒安全問題的原因

在多個執行緒併發環境下,多個執行緒共同訪問同一共享記憶體資源時,其中一個執行緒對資源進行寫操作的中途(寫入已經開始,但還沒 結束),其他執行緒對這個寫了一半的資源進行了讀操作,或者對這個寫了一半的資源進行了寫操作,導致此資源出現資料錯誤。

(二)、如何避免執行緒安全問題?

1、保證共享資源在同一時間只能由一個執行緒進行操作(原子性,有序性)。

2、將執行緒操作的結果及時重新整理,保證其他執行緒可以立即獲取到修改後的最新資料(可見性)。

相關文章