1. 程式人生 > >java中的執行緒安全與鎖優化

java中的執行緒安全與鎖優化

Java的執行緒是對映到作業系統的原生執行緒之上的,如果要阻塞或喚醒一條執行緒,都需要作業系統來幫忙完成,這就需要作業系統來幫忙完成,需要從使用者態轉換到核心態中,狀態轉換需要耗費很多的處理器時間。如果是非常簡單的程式碼同步塊,狀態轉換消耗的時間可能比使用者程式碼執行的時間還要長。 因此可以說,synchronized是Java語言中的一個重量級操作,對於有經驗的程式設計師都會在確實必要的情況下才使用這種操作,虛擬機器本身也會進行一些優化,譬如在通知作業系統阻塞執行緒之前加入一段自旋等待過程,避免頻繁地切入到核心態中。 除了synchronized之外,我們還可以使用JUC包中的重入鎖ReentrantLock來實現同步,它與synchronized類似,都具備一樣的執行緒重入特性,只是程式碼寫法上有點區別。ReentrantLock比synchronized增加了一些高階功能,主要有以下幾項:
  • 等待可中斷:當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情,對處理執行時間非常長的同步塊很有幫助;
  • 公平鎖:多個執行緒在等待同一個鎖時,必須按照申請鎖的順序來依次獲得鎖,非公平鎖不能保證這一點,鎖釋放時任何一個等待鎖的執行緒都有機會獲得鎖。synchronized中的鎖是非公平的。
  • 鎖繫結條件:一個ReetrantLock物件可以同時繫結多個condition物件,而在synchronized中,鎖物件的wait, notify方法可以實現一個隱含的條件,如果要和多於一個條件關聯的時候,就不得不額外新增一個鎖。而ReentrantLock不需要這麼做,只需要多次呼叫newCondition()即可。
互斥同步最主要的問題就是進行執行緒阻塞和喚醒帶來的效能問題,這種同步也被稱作阻塞同步,屬於一種悲觀的併發策略。隨著硬體指令集的發展,有了另外一個選擇:基於衝突檢測的樂觀併發策略,不需要將執行緒掛起,也被稱為非阻塞同步。 這種樂觀併發策略需要操作和衝突監測這兩個步驟具備原子性,只能靠硬體來完成這件事情,保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成,這類指令常用的有:
  • 測試並設定(Test-and-Set);
  • 獲取並增加(Fetch-and-Increment);
  • 交換(Swap);
  • 比較並交換(Compare-and-Swap, CAS);
  • 載入連結/條件儲存(Load-Linked/Store-Conditional, LL/SC);
鎖優化 JVM在monitorenter和monitorexit位元組碼依賴於底層作業系統mutex lock(互斥鎖)來實現的,但是由於使用mutex lock需要需要當前執行緒掛起並從使用者態切換到核心態來進行,切換代價非常昂貴。 在大部分的情況下,同步方法是執行在單執行緒環境,也就是無鎖競爭環境中,如果每次都呼叫mutex lock會嚴重影響效能,不過jdk1.6中對鎖的實現引入了大量的優化: 鎖粗化(Lock coarsening) 原則上來說我們編寫程式碼總是推薦將同步塊的作用範圍限制得儘量小,為了使得同步運算元量儘可能變小,如果存在鎖競爭,等待鎖的執行緒也能儘快地拿到鎖。但一系列的加鎖和解鎖,甚至加鎖操作出現在迴圈中,也會極大地影響效能。因此減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴充套件成一個範圍更大的鎖。

鎖消除
通過執行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他執行緒共享的資料的鎖保護。就是判斷一段程式碼中,在堆上的所有資料都不會逃逸出去被其他執行緒訪問到,就可以把它們當做棧上資料對待,認為它們是執行緒私有的,同步加鎖無須進行。 輕量級鎖
輕量級鎖:基於一種假設,即在真實的情況下我們程式中的大部分同步程式碼一般都處於無鎖競爭狀態,單執行緒執行環境,在無鎖競爭的情況下完全可以避免呼叫作業系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取以及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的執行緒將呼叫作業系統互斥鎖進行阻塞狀態,當鎖被釋放的時候被喚醒。
偏向鎖:為了在無鎖競爭的情況下避免在鎖獲取過程中執行不必要的CAS原子指令,因為CAS操作原子指令雖然相對於重量級鎖來說開銷比較小,但還是存在可觀的本地延遲。

自旋與適應性自旋 當執行緒在獲取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的作業系統重量級(mutex semaphore)前會進入忙等待,然後再次嘗試,當嘗試一定的次數後如果仍然沒有成功,則呼叫與該monitor關聯的semaphore(互斥鎖)進入阻塞狀態,可以使用-XX:+UseSpinning引數來開啟。自旋等待本身雖然避免了執行緒切換的開銷,但是需要佔用處理器時間的,如果鎖被佔用的時間很短,自旋等待效果就會很好,否則就是白白浪費處理器資源,不會做任何有用工作反而帶來效能上的浪費。自旋次數的預設值=10,可以使用-XX:PreBlockSpin來更改,適應性自旋的時間就不會固定了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者狀態來決定。 輕量級鎖 對於每個被鎖住的物件(java中的所有鎖的地方都是加在某個物件上的,無論是具體物件還是class),都會和一個monitor record關聯,物件頭中的LockWord指向monitor record起始地址,同時monitor record中有一個owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。 Java物件記憶體佈局 物件在記憶體中的儲存部分分成三個部分: 1.物件頭,物件頭中自身的執行時資料,Mark Word(在32bit和64bit虛擬機器長度分別為32bit和64bit),主要包括以下的資訊:
  • 物件hashCode;
  • 物件GC分代年齡;
  • 鎖狀態標誌(輕量級鎖,重量級鎖);
  • 執行緒持有的鎖(輕量級鎖,重量級鎖);
  • 偏向鎖相關,偏向鎖,自旋鎖,輕量級鎖以及其他的一些鎖優化策略是jdb1.6加入的,這些優化使得synchronized的效能與ReentrantLock的效能持平,在synchronized可以滿足要求的情況下,優先使用synchronized,除非是使用一些ReetrantLock獨有的功能,比如指定時間等待等。
例如在32位的hotspot虛擬機器中物件未被鎖定的情況下,mark word的32bits中25bits用於儲存物件hashCode,4bits用於儲存物件分代年齡,2bits用於儲存鎖標誌位,1bit固定為0

  此外物件頭中還包括型別指標,物件通過指向元資料的指標,JVM通過這個指標來確定這個物件是哪個類的例項; 2.例項資料,物件真正儲存的資料,有效資訊; 3.對齊填充,JVM要求物件大小必須是8的整數倍,如果不是則需要補位。 需要注意的是,mark word具有非固定的資料結構,以便在極小的空間記憶體儲儘量多的資訊;如果物件是一個數組,物件頭必須有一塊兒用於記錄陣列長度的資料,JVM可以通過Java物件的元資料確定物件長度,但是對於陣列則不行;基本的資料型別中佔用的記憶體大小:

  輕量級鎖和偏向鎖 理解什麼是偏向鎖之前,必須要先理解什麼是輕量級鎖(lightweight locking)。引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,由於一旦出現多執行緒競爭的情況,就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能損耗。在JDK1.6以後預設開始了偏向鎖的優化,可以通過啟動JVM的時候加入 -XX:-UseBiasedLocking引數來禁用偏向鎖,在存在大量鎖物件的建立並高度併發的環境下禁用偏向鎖能夠帶來一定的效能優化。 輕量級鎖的執行過程:在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(標誌位為01狀態),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄的空間,用於儲存鎖物件目前mark word的拷貝。然後虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,如果更新成功該執行緒擁有該物件的鎖,將物件mark word鎖標誌位轉變為00,輕量級鎖定狀態;如果更新失敗,虛擬機器首先檢查該物件的mark word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有該物件的鎖,可直接進入同步塊繼續執行,否則說明這個鎖物件已經被其他執行緒搶佔。 如果有兩條以上的執行緒徵用同一個鎖,則輕量級鎖不再有效,膨脹為重量級鎖,狀態值轉換為10,後面等待鎖的執行緒也要進入阻塞狀態。此中的操作都是使用CAS嘗試來進行比較並交換標誌位。 輕量級鎖提升程式同步效能的依據是:對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,但如果鎖競爭較為激烈,除了互斥量的開銷還額外發生了CAS操作,輕量級鎖此時會比傳統的重量級鎖更慢。 偏向鎖的目的是消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去除同步使用的互斥量,那偏向鎖就是在無競爭情況下把整個同步都消除掉,連CAS操作都不做了。 假如當前虛擬機器設定了偏向模式,當鎖物件第一次被執行緒獲取的時候,虛擬機器將會把物件頭中的標誌位設定為01,即偏向模式,同時使用CAS操作把獲取到這個鎖的執行緒ID記錄在物件的Mark Word之中,如果CAS操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器都可以不再進行任何同步操作。 當另外的執行緒去嘗試獲取該鎖時,偏向模式宣告結束,根據鎖物件目前是否處於鎖定狀態,撤銷偏向後恢復到未鎖定或輕量級鎖定狀態,後續的同步操作就如輕量級鎖來執行。 偏向鎖可以提高帶有同步但無競爭的程式效能,但並不一定總是對程式有力,如果大多數的鎖總是被多個不同執行緒訪問,偏向模式就是多餘的。