1. 程式人生 > >Java 中執行緒安全問題

Java 中執行緒安全問題

不好意思,一個國慶假期給我放的都不知道東西南北了,放鬆,很放鬆,差一點就棄更了,感謝那些催更的小夥伴們!

雖然沒有更新,但是日常的學習還是有的,以後我儘量給大家分享一些通用知識,非技術。

但是本期還是要回歸到之前的多前程的話題。已經說了執行緒和程序的區別、如何實現多執行緒、今天說一說執行緒中的安全問題。

首先明確一個概念,我們說執行緒安全是預設在多執行緒環境中,因為單執行緒中不存線上程安全問題。執行緒安全體現在多執行緒環境中程式的執行結果和單執行緒執行的結果一樣。

那麼多執行緒中會存在神馬問題呢?舉個例子來說,下面這一段程式碼中存在一個共享變數 count 。當程式呼叫 add 方法的時候,我們無法確定 count 的值是多少,因為它可能已經被其它執行緒 add 了很多次,或是正在被 add……

public class Obj {
    private int count;

    public int add() {
        return ++count;
    }
}

這種情況是堅決不允許的!除非你就是想計算 add 方法被呼叫的次數。那我們要保證同一時間只能有一個執行緒訪問共享資源,這也就是在實現執行緒安全。

如何實現呢?最常見的方式是加鎖和同步。

使用鎖機制可以保證同一時刻只有一個執行緒可以拿到鎖進入臨界區,保證了上鎖和釋放鎖之間的這段程式碼同一時刻只能被一個執行緒執行。

public void testLock () {
    lock.lock();
    
try{ int j = i; i = j + 1; } finally { lock.unlock(); } }

與鎖類似的是同步方法或者同步程式碼塊。使用非靜態同步方法時,鎖住的是當前例項;使用靜態同步方法時,鎖住的是該類的 Class 物件;使用靜態程式碼塊時,鎖住的是 synchronized 關鍵字後面括號內的物件。

public void testLock () {
    synchronized (anyObject){
        int j = i;
        i = j + 1;
    }
}

無論使用鎖還是 synchronized,本質都是一樣,通過鎖或同步來實現資源的排它性,從而實際目的碼段同一時間只會被一個執行緒執行,進而保證了目的碼段的原子性。這是一種以犧牲效能為代價的方法。

除了上面的兩種方式還有一起其它的方法,比方說使用關鍵字 volatile 來修飾共享變數,或是保證每一步操作的原子性,但是在實際開發中不建議使用這些方式來保證執行緒的安全。

那我們來看一看,保證執行緒安全到底是在保證什麼?底層的邏輯又是什麼呢?

執行緒安全的三大特性,原子性、可見性、有序性。

原子性:類似於資料庫中說的原子性,不可分割的操縱。這裡說的就是對臨界區程式碼的原子性操作,保證了執行緒的安全。加鎖和同步都是在實現原子性。

可見性:當一個執行緒修改了變數後立即同步到主存中,讓其它執行緒知道這個變數已經被改變了。就不會讀取到過時的資料。

Java 提供了 volatile 關鍵字來保證可見性。當使用 volatile 修飾某個變數時,它會保證對該變數的修改會立即被更新到記憶體中,並且將其它執行緒快取中對該變數的快取設定成無效,因此其它執行緒需要讀取該值時必須從主記憶體中讀取,從而得到最新的值。

有序性:在程式的執行順序和程式碼的編寫順序一樣。這就是有序性,但是Java虛擬機器為了優化,有一種重排序的機制,也就是說程式碼的執行順序不一定是語句的順序不一致,這在單執行緒中不會存在問題,JVM 會保證執行結果的正確。然而在多執行緒中,就會出現問題。

Java 中可通過 volatile 在一定程式上保證有序性,另外還可以通過 synchronized 和鎖來保證有序性。

synchronized 和鎖保證有序性的原理和保證原子性一樣,都是通過保證同一時間只會有一個執行緒執行目的碼段來實現的。

JVM 為了優化執行順序,提高效能。設計了重排序機制,然而這個機制在多執行緒中可能會帶來問題,為了解決這個問題,我們可以通過程式碼保證有序性。這還不夠, JVM 本身存在一個機制 happens-before(先行執行)來保證程式執行順序。其餘的程式碼就隨 JVM 怎麼弄了,目的提高效能。

先行執行機制中定義了好幾條規則,拿出幾條參觀一下,你會感覺,呦,原來你認為很自然的事情是被這個規則所定義的!

1 被 volatile 修飾的寫操作先發生於後面對該變數的讀操作。

2 一個執行緒內,按照程式碼順序執行。

3 Thread 物件的 start() 方法先發生於此執行緒的其它動作。

4 一個物件構造先於它的 finalize 發生。

……

總結一下,執行緒安全問題發生在多執行緒環境下對共享變數的操作中,為了防止出現數據狀態不一致的情況,我們可以不在多執行緒中共享變數、將變數設定為 volatile 、使用同步或鎖。其實也是在保證執行緒的特性不受破壞……