JVM(5)-線程安全與鎖優化 1.線程安全 2.Java語言中的線程安全 3.線程安全的實現方法 4.鎖優化

1.線程安全

如果一個對象能安全地被多個線程同時使用,那麼它就是線程安全的。

當多個線程訪問同一個對象時,如果不需要考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都能得到正確的結果,那這個對象就是是線程安全的。

2.Java語言中的線程安全

2.1不可變

JDK1.5後,不可變(Immutable)對象一定是線程安全的,注意final修飾的基本數據類型是不可變的,但是引用類型只能保證一級不可變,即當前引用不可再賦值,但引用的對象的非final屬性是可以修改的。

2.2絕對線程安全

無論怎麼使用,都是線程安全的,滿足上面第二個定義。

2.3相對線程安全

比如Vector,雖然它的方法加了synchronized關鍵字,但並不能說它就是絕對安全的。

2.4線程兼容

對象本身不是線程安全的,但是可以通過一定的同步來實現線程安全,例如synchronized。

2.5線程對立

如 Thread.suspend()和Thread.resume()

3.線程安全的實現方法

同步是指在多線程併發訪問共享數據時,保證同一時刻只被一個(或者是一些,使用信號量時)線程使用。

3.1互斥同步

互斥是實現同步的手段,臨界區、互斥量、信號量都是主要的互斥實現方式。Java中最基本的互斥手段就是synchronized關鍵字,synchronized關鍵字在編譯後,會在同步塊前後分別形成monitorenter和monitorexit指令。這兩個指令需要一個reference類型的參數來指明要鎖定和解鎖的對象。如果synchronized明確指定了對象參數,那就是這個對象的reference,如果沒有指定,那就要區分是實例方法還是類方法,取當前對象或者這個類對應的Class對象。

3.2非阻塞同步

互斥同步的主要問題是進行線程的阻塞和喚醒所帶來的性能問題,因爲這種方式會阻塞其他線程,因此也可以稱爲阻塞同步。從處理方式上來看,屬於悲觀的併發策略,即不管有沒有競爭,都進行同步。
隨着硬件指令系統的發展(需要一些原子操作指令的支持,例如CAS,早期的計算機指令系統可能沒有這樣的指令),有了另一個選擇,基於衝突檢測的樂觀併發策略,即先進形操作,如果沒有其他線程爭用共享數據,那就操作成功了,如果存在競爭,再採取必要的補救措施,比如不斷地重試,直到成功爲止。

4.鎖優化

4.1自旋鎖與自適應自旋

由於掛起線程和喚醒線程需要切換的內核狀態進行,這是個不小的開銷。虛擬機的開發團隊研究發現許多應用,共享鎖的鎖定轉態通常是持續很短的時間,所以可以讓在等待獲鎖的線程不進入阻塞狀態,而是繼續執行自旋操作,稍等一下就能獲得鎖,這樣做在一定程度上避免用戶態與內核態的切換,但自旋的線程會繼續佔用CPU時間片。

自旋時間的選擇也是很關鍵的,自旋多久後仍然沒有獲得鎖就進入阻塞狀態?所以在JDK1.6後,引入了自適應的自旋鎖,由前一次在同一個鎖上的自旋時間和鎖的擁有線程的狀態決定。

4.2鎖消除

虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在數據競爭的鎖進行清除。這種通常不是開發人員自己寫出來的,舉一個例子:

    public String concatString(String s1, String s2, String s3) {
        return s1 + s2 + s3;
    }

由於String是一個不可變的類,在字符串連接時都是創建一個新的String對象來完成的,因此,javac編譯器對String對象連接做了自動化。在JDK1.5以前,是通過StringBuffer來完成,而JDK1.5及之後,是通過StringBuilder來完成。那JDK1.5之前上面的代碼就會變成:


    public String concatString(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

而StringBuffer的append()方法是加了synchronized關鍵字的同步方法,但是很顯然這種情況下的同步是完全沒有必要的,所以虛擬機將這種鎖清除掉以提高性能。

4.3鎖粗化

同步有一個原則是,讓同步塊儘量小一下,一般情況下是正確的,但如果一系列的操作都對同一個對象進行加鎖解鎖,會帶來不小的性能開銷,這種情況還不如把同步範圍擴大至一系列操作之前,這樣只需要加/解鎖一次就行了。

4.4輕量鎖

前面《運行時數據區》一文中提到對象的內存佈局包括3部分:

對象頭(Header)
實例數據(Instance Data)
對齊填充(Padding)非必須

HotSpot虛擬機的對象頭(Object Header)包含兩部分的信息:

  • 第一部分用戶存儲對象自身的運行時數據,如 HashCode、GC分代年齡(Generational GC Age)等,這部分數據在32bit和64bit的虛擬機中分別爲32bit和64bit,官方稱它爲“Mark Word”,它是輕量級鎖和偏向鎖的關鍵;
  • 另外一部分用於存儲方法區對象類型的引用,如果是數組對象的話,還會有一個額外的部分用於存儲數組的長度。

32bit HotSpot虛擬機下的對象狀態,爲鎖定狀態下Mark Word 32bit中,25bit是HashCode,4bit是對象分代年齡,2bit是標記(例如未鎖定是01),1bit固定爲0。

存儲內容 標記位 狀態
對象哈希碼,分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不需要記錄信息 11 GC標記
偏向線程ID,偏向時間戳,對象分代年齡 01 可偏向
  1. 在代碼進入同步代碼塊時,如果對象是未鎖定狀態,虛擬機會首先會在當前線程的棧幀中創建一個鎖記錄(Locked Record)空間,用於存儲鎖對象當前的Mark Word拷貝,叫Displaced Mark Word;
  2. 採用CAS操作將鎖對象的Mark Word修改爲指向鎖記錄的指針,如果更新成功,那線程就擁有了該鎖對象,並且對象的Mark Word的標記位(最後2bit)修改爲00。

輕量級鎖提升性能的依據是:對於絕大部分的鎖,同步過程是不存在競爭的。如果沒有競爭,那輕量級的CAS操作避免了互斥量的開銷,但如果存在競爭,那性能反而傳統的重量級鎖慢(CAS+互斥信號量)。

4.5偏量鎖

如果說輕量級鎖是在無競爭條件下,通過CAS操作去消除的同步使用互斥信號量,那偏向鎖就是在無競爭條件下把整個同步都消除掉,連CAS也不用做。

“偏”指的是這個鎖對象會偏向第一個獲取它的線程,如果在接下來的的執行過程中,該鎖沒有被其他線程獲取,則持有該偏向鎖的線程將永遠不再需要同步。

當鎖對象第一次被獲取時,標記爲被設爲“01”,即偏向模式,並且把獲取到這個鎖的線程ID記錄在Mark Word中,後面持有偏向鎖的線程在進入同步代碼塊,就不需要再同步了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章