1. 程式人生 > >Java併發讀書筆記:執行緒安全與互斥同步

Java併發讀書筆記:執行緒安全與互斥同步

目錄

  • 導致執行緒不安全的原因
  • 什麼是執行緒安全
    • 不可變
    • 絕對執行緒安全
    • 相對執行緒安全
    • 執行緒相容
    • 執行緒對立
  • 互斥同步實現執行緒安全
    • synchronized內建鎖
      • 鎖即物件
      • 是否要釋放鎖
      • 實現原理
      • 啥是重進入?
    • ReentrantLock(重入鎖)
      • API層面的互斥鎖
      • 等待可中斷
      • 公平鎖
      • 鎖繫結

本篇參考許多著名的書籍,形成讀書筆記,便於加深記憶。

前文傳送門:Java併發讀書筆記:JMM與重排序

導致執行緒不安全的原因

當一個變數被多個執行緒讀取,且至少被一個執行緒寫入時,如果讀寫操作不遵循happens-before規則,那麼就會存在資料競爭的隱患,如果不給予正確的同步手段,將會導致執行緒不安全。

什麼是執行緒安全

Brian Goetz在《Java併發程式設計實戰》中是這樣定義的:

當多個執行緒訪問一個類時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,並且不需要額外的同步及在呼叫方程式碼不必做其他的協調,這個類的行為仍然是正確的,那麼這個類就是執行緒安全的。


周志明在《深入理解Java虛擬機器》中提到:多個執行緒之間存在共享資料時,這些資料可以按照執行緒安全程度進行分類:

不可變

不可變的物件一定是執行緒安全的,只要一個不可變的物件被正確地構建出來,那麼它在多個執行緒中的狀態就是一致的。例如用final關鍵字修飾物件:

  • 修飾的是基本資料型別,final修飾不可變。
  • 修飾的是一個物件,就需要保證其狀態不發生變化。

JavaAPI中符合不可變要求的型別:String類,列舉類,數值包裝型別(如Double)和大資料型別(BigDecimal)。

絕對執行緒安全

即完全滿足上述對於執行緒安全定義的。

滿足該定義其實需要付出很多代價,Java中標註執行緒安全的類,實際上絕大多數都不是執行緒安全的(如Vector),因為它仍需要在呼叫端做好同步措施。Java中絕對執行緒安全的類:CopyOnWriteArrayList

CopyOnWriteArraySet

相對執行緒安全

即我們通常所說的執行緒安全,Java中大部分的執行緒安全類都屬於該範疇,如VectorHashTableCollections集合工具類的synchronizedCollection()方法包裝的集合等等。就拿Vector舉例:如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制。

執行緒相容

物件本身並不是執行緒安全的,可以通過在呼叫段正確同步保證物件在併發環境下安全使用。如我們之前學的分別與Vector和HashTable對應的ArrayListHashMap


物件通過synchronized關鍵字修飾,達到同步效果,本身是安全的,但相對來說,效率會低很多。

執行緒對立

無論呼叫端是否採取同步措施,都無法正確地在多執行緒環境下執行。Java典型的執行緒對立:Thread類中的suspend()和resume()方法:如果兩個執行緒同時操控一個執行緒物件,一個嘗試掛起,一個嘗試恢復,將會存在死鎖風險,已經被棄用。

常見的對立:System.setIn()System.setOut()System.runFinalizersOnExit()

互斥同步實現執行緒安全

互斥同步也被稱做阻塞同步(因為互斥同步會因為執行緒阻塞和喚醒產生效能問題),它是實現執行緒安全的其中一種方法,還有一種是非阻塞同步,之後再做學習。

互斥同步:保證併發下,共享資料在同一時刻只被一個執行緒使用。

synchronized內建鎖

其中使用synchronized關鍵字修飾方法或程式碼塊是最基本的互斥同步手段。

synchronized是Java提供的一種強制原子性的內建鎖機制,以synchronized程式碼塊的定義方式來說:

synchronized(lock){
    //訪問或修改被鎖保護的共享狀態
}

它包含了兩部分:1、鎖物件的引用 2、鎖保護的程式碼塊。

每個Java物件都可以作為用於同步的鎖物件,我們稱該類的鎖為監視器鎖(monitor locks),也被稱作內建鎖。

可以這樣理解:執行緒在進入synchronized之前需要獲得這個鎖物件,線上程正常結束或者丟擲異常都會釋放這個鎖。

而這個鎖物件很好地完成了互斥,假設A持有鎖,這時如果B也想訪問這個鎖,B就會陷入阻塞。A釋放了鎖之後,B才可能停止阻塞。

鎖即物件

  • 對於普通同步方法,鎖是當前例項物件(this)。
//普通同步方法
public synchronized void do(){}
  • 對於靜態同步方法,鎖是當前的類的Class物件。
//靜態同步方法
public static synchronized void f(){}
  • 對於同步方法塊,鎖的是括號裡配置的物件。
//鎖物件為TestLock的類物件
synchronized (TestLock.class){    
    f();
}

明確:synchronized方法和程式碼塊本質上沒啥不同,方法只是對跨越整個方法體的程式碼塊的簡短描述,而這個鎖是方法所在物件本身(static修飾的方法,物件是當前類物件)。這個部分可以參考:Java併發之synchronized深度解析

是否要釋放鎖

釋放鎖的情況:

  • 執行緒執行完畢。
  • 遇到return、break終止。
  • 丟擲未處理的異常或錯誤。
  • 呼叫了當前物件的wait()方法。

不釋放鎖的情況:

  • 呼叫了Thread.sleep()和Thread.yield()暫停執行不會釋放鎖。
  • 呼叫suspend()掛起執行緒,不會釋放鎖,已被棄用。

實現原理

JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但兩者實現細節不同。

程式碼塊同步使用monitorentermonitorexit兩個指令實現,JVM的要求如下:

  • monitorenter指令會在編譯後插入到同步程式碼塊的開始位置,而monitorexit則會插入到方法結束和異常處。
  • 每個物件都有一個monitor與之關聯,且當一個monitor被持有之後,他會處於鎖定狀態。
  • 執行緒執行到monitorenter時,會嘗試獲取物件對應monitor的所有權。

  • 在獲取鎖時,如果物件沒被鎖定,或者當前執行緒已經擁有了該物件的鎖(可重進入,不會鎖死自己),將鎖計數器加一,執行monitorexit時,鎖計數器減一,計數為零則鎖釋放。
  • 獲取物件鎖失敗,則當前執行緒陷入阻塞,直到物件鎖被另外一個執行緒釋放。

啥是重進入?

重進入意味著:任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖阻塞,synchronized是隱式支援重進入的,因此不會出現鎖死自己的情況。

這就體現了鎖計數器的作用:獲得一次鎖加一,釋放一次鎖減一,無論獲得還是釋放多少次,只要計數為零,就意味著鎖被成功釋放。

ReentrantLock(重入鎖)

ReentrantLock位於java.util.concurrent(J.U.C)包下,是Lock介面的實現類。基本用法與synchronized相似,都具備可重入互斥的特性,但擁有擴充套件的功能。

Lock介面的實現提供了比使用synchronized方法和程式碼塊更廣泛的鎖操作。允許更靈活的結構,具有完全不同的屬性,並且可能支援多個關聯的Condition物件。

RenntrantLock官方推薦的基本寫法:

class X {
    //定義鎖物件
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定義需要保證執行緒安全的方法
    public void m() {
        //加鎖
        lock.lock();  
        try{
        // 保證執行緒安全的程式碼
        }
        // 使用finally塊保證釋放鎖
        finally {
            lock.unlock()
        }
    }
}

API層面的互斥鎖

ReentrantLock表現為API層面的互斥鎖,通過lock()unlock()方法完成,是顯式的,而synchronized表現為原生語法層面的互斥鎖,是隱式的。

等待可中斷

當持有執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待或處理其他事情。

公平鎖

ReentrantLock鎖是公平鎖,即保證等待的多個執行緒按照申請鎖的時間順序依次獲得鎖,而synchronized是不公平鎖。

鎖繫結

一個ReentrantLock物件可以同時繫結多個Condition物件。


JDK1.6之前,ReentrantLock在效能方面是要領先於synchronized鎖的,但是JDK1.6版本實現了各種鎖優化技術,後續效能改進會更加偏向於原生的synchronized。

參考資料:《Java併發程式設計實戰》、《Java併發程式設計的藝術》、《深入理解Java虛擬