Java執行緒安全小結

youqi發表於2018-07-10

一、引言

Java開發過程中許多的時候都會涉及到各種各樣的併發程式設計的問題,然而說起併發程式設計總需要格外的關注執行緒安全的問題。最近呢一直在基於Jstorm開發日誌處理程式,由於Jstorm的特性,多執行緒隨處可見。所以程式中也需要特別關注執行緒安全的問題。這次專案開發過程也遇到了不少的問題,通過不斷的查詢資料,不斷的修改問題也確實收穫了不少的知識。因此寫一下最近關於併發程式設計的學習和總結。

二、多執行緒基礎

在併發程式設計中,執行緒和鎖起著至關重要的作用,要完成健壯的併發程式就必須要正確的使用執行緒和鎖。在我的實戰過程中我覺得有兩點需要我們特別的注意。

  1. 對可變共享資源的操作
  2. 當前的鎖物件

這兩個地方不考慮清楚很容易寫出執行緒不安全的程式碼。後面我會結合相關的示例來說明這一點。

術語

共享資源:能夠被多個執行緒同時訪問的資源
競態條件:當兩個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件
臨界區:導致競態條件發生的程式碼區

什麼是執行緒安全?

在併發程式設計中我們最為關心的便是執行緒安全的問題。只有執行緒安全的程式在併發程式設計中才有用武之地。

那麼,到底什麼是執行緒安全呢?

當多個執行緒訪問某個類時,這個類始終都能表現出正確的行為,那麼就稱這個類時執行緒安全的。

上面這句話給出了執行緒安全的一個定義,然而不同的實際場景這個正確的行為可能各不相同,需要我們開發去主觀的判斷。我在實戰過程中往往會在每個操作執行過程中去考慮此時共享資源所處的狀態,然後不斷的測試程式碼的執行,檢視中間過程共享資源的真實狀態,從而實現出執行緒安全的程式碼。

三、關於執行緒安全的實戰

上面概括的講了一些基礎的知識,接下來紀錄下最近工作中遇到的問題。在基礎中我強調了兩個注意事項,在接下來的示例中我們就能夠發現弄清楚程式碼中對共享資源的操作和當前同步中獲得的所物件是什麼對寫出執行緒安全的程式碼的重要性。

字串常量作為物件鎖

bingfa1
再次考慮兩個注意點,第一,共享資源是什麼;第二當前的物件鎖是什麼。
上圖中的list集合即為共享資源,同步程式碼塊中是對共享的操作。而當前的synchronized獲取的鎖則為字串s。由於以上圖中建立的字串直接儲存在jvm的常量池中,而且一個jvm中只儲存一份。因此每個執行緒要想訪問同步程式碼塊中的程式碼都需要獲得字串s的鎖。所以多個執行緒競爭同一個鎖,從而到達每次只有一個執行緒可以訪問同步程式碼塊中的程式碼,即執行緒安全。

bingfa2
這次程式碼只改動了很小一部分,但是結果卻是很出乎意料,現在這段程式碼程式設計了執行緒不安全的了。當時出現這個問題的時候我們也是有點意外。
遇到問題,首先判斷哪些地方操作了共享資源list,檢查整個程式碼發現只有圖中的同步程式碼塊中的程式碼。於是判斷,共享資源的操作都在同步程式碼塊中,共享資源的操作不是問題的關鍵。那邊應該就是第二個條件物件鎖出現了問題。對比圖一的程式碼,圖一之所以執行緒安全是因為所物件只有一個,每個執行緒去競爭同一個物件鎖,從而保證同一時間只有一個執行緒執行同步程式碼塊中的程式碼。所以猜測圖二中的s物件不唯一,每個執行緒的鎖住的物件各不相同,從而導致同一時間有多個執行緒執行了同步程式碼塊中的程式碼。

接下來我們看看圖二中位元組碼反編譯後的結果。

bingfa3
圖中可以看到編譯器對字串操作符進行了處理,在用+連線兩個字串時編譯器最終會通過StringBuilder的append方法拼接,最後通過呼叫toString方法獲取字串,因此不難理解這裡每個執行緒都會new出自己的StringBuilder物件,所以這裡每個執行緒獲取的物件鎖都是自己的StringBuilder物件,並沒有去競爭同一個物件鎖。從而造成了執行緒不安全的產生。

上面這個示例主要是錯誤的鎖物件的使用造成了執行緒不安全。

錯用this關鍵字

在使用同步的過程中經常會看到synchronized(this)的寫法。此時應該要明白這裡this的真正代表的含義。這裡this表示當前物件,而synchronized(this)則表示需要需要獲取當前物件的鎖,才行訪問同步程式碼塊中的物件。因此synchronized(this)只適用多執行緒共用同一個例項

public class SyncTest implements Runnable {
    private static final List<String> listA = new ArrayList<>();
    private static final List<String> listB = new ArrayList<>();
    private static final List<String> listC = new ArrayList<>();
    private static final List<String> listD = new ArrayList<>();

    @Override
    public void run() {
        while (true) {
            List<String> list = null;
            TestEnum testEnum = TestEnum.getEnumByTime();
            switch (testEnum) {
                case aaa:
                    list = listA;
                    break;
                case bbb:
                    list = listB;
                    break;
                case ccc:
                    list = listC;
                    break;
                case ddd:
                    list = listD;
                    break;
            }
          
            synchronized (this) {
            /*在多例項併發訪問的時候會出現執行緒不安全的現象,
            因為每個例項競爭的物件鎖不是同一個,但是static的變數所有例項共享
            */
                list.add(Thread.currentThread().getName() + ","
                        + System.currentTimeMillis());

                for (String a : list) {
                    System.out.println(a + ",size = " + list.size());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

四、總結

理論知識只有運用在實踐過程中才能更加深刻更加透徹的被理解。線上程安全的問題出現時,搞清兩個重點去排查問題的手段也非常的實用。併發程式設計的第一要務便是安全,在安全的基礎之上才是效能。總之,最近基於Jstorm開發程式使自己多執行緒的理解又更加深了一些。在工作中深度學習點滴技術。


相關文章