鎖的種類 獨享鎖 VS 共用鎖 獨享鎖:鎖只能被一個線程持有(synchronized) 共用鎖:鎖可以被多個程式所持有(讀寫鎖) 樂觀鎖 VS 悲觀鎖 樂觀鎖:每次去拿數據的時候都樂觀地認為別人不會修改,所以不進行加鎖操作。樂觀鎖適用於多讀的應用類型。(CAS,Atomic) CAS(Compar ...
鎖的種類
- 獨享鎖 VS 共用鎖
- 獨享鎖:鎖只能被一個線程持有(synchronized)
- 共用鎖:鎖可以被多個程式所持有(讀寫鎖)
- 樂觀鎖 VS 悲觀鎖
- 樂觀鎖:每次去拿數據的時候都樂觀地認為別人不會修改,所以不進行加鎖操作。樂觀鎖適用於多讀的應用類型。(CAS,Atomic)
- CAS(Compare And Swap),其思想是:我認為V的值應該為 A,如果是,那麼將 V 的值更新為 B,否則不修改並告訴V的值實際為多少。這樣一來當有多個線程嘗試修改同一個對象時,只有一個線程能夠成功修改,因為一旦有一個線程修改成功了,那麼其他線程就沒法滿足 V 的值是 A 了。其他線程修改失敗之後不會被掛起,而是再次嘗試修改。
- 悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
- 樂觀鎖:每次去拿數據的時候都樂觀地認為別人不會修改,所以不進行加鎖操作。樂觀鎖適用於多讀的應用類型。(CAS,Atomic)
- 公平鎖 VS 非公平鎖
- 公平鎖:對於等待鎖隊列中的線程,按照先來先得的原則,給予鎖。
- 非公平鎖:一個線程一執行到需要鎖的地方,就嘗試去要鎖,失敗了再進等待隊列。這樣一來,就能夠大大減少喚醒線程的開銷,但有可能會出現某線程餓死的情況。
- synchronized 是非公平鎖,lock 可以是公平也可以非公平。
- 其他概念
- 可重入鎖(synchronized 和 lock):可重入性表明瞭鎖的分配不是基於方法而是基於線程的。如:synchronized 了兩個方法 method1, method2,method1 中調用了 method2,那麼只要線程獲得了 method1 的鎖,那麼它就一定能夠進入 method2 而不需再申請鎖。
- 可中斷鎖(lockInterruptible()):對於等待鎖的線程,若等待時間過長,想中斷該等待過程,讓該線程去做其他的事,那麼就需要可中斷鎖。
Java 鎖的狀態
Java 鎖狀態的級別從低到高為:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重要級鎖狀態,而鎖的狀態是被記錄在 Java 對象的對象頭中。
Java 的對象頭:由 Mark Word、指向類的指針、數組長度,其中 Mark Word 負責記錄有關鎖的信息
32 位 JVM 中 Mark Word 存儲的內容為
鎖狀態 25bit 4bit 1bit (是否是偏向鎖) 2bit (鎖標誌位) 無鎖 對象的 HashCode 分代年齡 0 01 偏向鎖 線程 ID 與 Epoch 分代年齡 1 01 輕量級鎖 指向棧中的鎖記錄指針 同前 同前 00 重量級鎖 指向重量級鎖的指針 同前 同前 10 GC 標記 空 11
偏向鎖、輕量級鎖概念的出現,都是為了減少同步喚醒的代價,若所有鎖都為重量級的,那麼所有等待鎖的線程必須進入阻塞態
- 偏向鎖:大多數情況下,鎖並不存在被大量線程競爭,而且總是由一個線程多次重覆申請,那麼這時只需要讓偏向鎖記錄該線程 ID,這樣當該線程又來聲請鎖的時候,就能快速獲得鎖了。
- 輕量級鎖:存在競爭,但是不強力且持有鎖的線程會很快釋放鎖,在這種情況下,等待鎖的線程可以處於自旋狀態,而不必進入阻塞狀態。
Java 中鎖的狀態是一級一級往上升的,具體情況為:
- 當未遇到同步代碼塊(即沒有出現鎖的時候),處於無鎖狀態。
- 當對象被當作了同步鎖,但當前只有一個線程 A 申請了該鎖(沒有競爭產生),那麼對象的鎖狀態就會升級為偏向鎖。
- 當又有另一線程 B 來申請鎖時,這時會檢查當前占用該鎖的線程 A 是否處於活躍狀態或者仍然需要該鎖,即是否有競爭會產生。若無,則偏向鎖偏向 B,A 釋放偏向鎖。
- 若有,那麼偏向鎖就會升級會輕量級鎖。但若競爭進一步加大(有很多線程需求鎖,且占用時間較長),那麼鎖就會進一步升級為重量級鎖。
Java 鎖機制的實現
synchronized
從上一節我們可以看出來,Java 中能夠作為鎖的只能是對象,所以 synchronized 的實現,也是通過對對象進行加鎖實現的。
- synchronized 的三種用法
- synchronized aMethod,鎖一個類的方法,可以防止多個線程同時調用同一個對象的這個方法,但是能夠同時調用。
- static synchronized aMethod,鎖一個類的靜態方法,可以防止多個線程同時這個類的該靜態方法。
- synchronized (object){代碼塊},把 object 對象當作該代碼塊的鎖
- synchronized 使用起來簡單,但是有如下缺點
- 不支持中斷,可能造成其他線程等待很長的時間,甚至死鎖。
- 不支持讀寫鎖,即讀讀操作是能夠同時進行的,但是 synchronized 沒法實現。
Lock
Lock 是 java.util.concurrent.locks 包里的一個介面,對應的 ReentrantLock 類實現了該介面。
一般使用方法
public class LockTest { private Lock alock = new ReentrantLock(); // 鎖需要定義在具體使用鎖的棧幀的上一層,即若線上程里定義的話,不同的線程會 new 出不同的鎖,這樣就不能實現同步了。 public static void main(String[] args) { Runnable r = () -> { alock.lock(); // 獲得鎖 try { // do something } catch(Exception e){ } finally{ alock.unlock(); } } Thread test = new Thread(r); test.start(); } }
獲得鎖的方法
- lock(): 獲取鎖,若失敗,則等待
- tryLock(long time, TimeUnit unit): 嘗試獲得鎖,返回一個布爾值,表明是否獲得到了鎖。
- lockInterruptibly(): 若某一個線程使用這個方法等待獲取鎖,那麼可以通過 thread.interrupt() 的方法去讓他中斷等待,乾別的事。
除此之外,java.util.concurrent.locks 包內還定義有 ReadWriteLock 介面,並有多種讀寫鎖的實現類。
volatile
一種輕量級同步機制,能夠保證 volatile 修飾的變數具有可見性,但不具備原子性
- 原子性與可見性
- 原子性:在多線程併發的條件下,對於變數的操作是線程安全的,不會受到其他線程的干擾。Atomatic 基於底層硬體處理器提供的原子指令,保證併發時線程安全。
- 可見性:在多線程併發的條件下,對於變數的修改,其他線程中能獲取到修改後的。volatile 通過對於值的操作,會立即更新到主存中,當其他線程獲取該值會從主存中獲取。
- 使用方法與 synchronized 一樣,都是 Java 關鍵詞。
Atomatic
前面提到過 Atomatic 是一種樂觀鎖,它是通過操作的原子性來保證自己的線程安全的,即操作是不可被打斷的,且具有排他性。當某一線程進行該原子操作時,其他想要執行該操作的線程只能處於自旋狀態,等待該操作完成。
- 其實,這個給人的感覺,atomatic 有點像輕量級鎖,不過這種線程的等待,不是通過軟體實現的,而是通過硬體實現的(硬體上的原子操作)。
- 不過這種依靠硬體上的原子操作實現的鎖機制,導致 atomatic 的相關操作都比較基礎。
- 使用方法:參見 java.util.concurrent.atomic