本文章轉自:樂位元組
文章主要講解:Java鎖
擷取更多Java相關資料可以關注公衆号《樂位元組》 發送:999
悲觀鎖對應于生活中悲觀的人,悲觀的人總是想着事情往壞的方向發展。
舉個生活中的例子,假設廁所隻有一個坑位了,悲觀鎖上廁所會第一時間把門反鎖上,這樣其他人上廁所隻能在門外等候,這種狀态就是「阻塞」了。
回到代碼世界中,一個共享資料加了悲觀鎖,那線程每次想操作這個資料前都會假設其他線程也可能會操作這個資料,是以每次操作前都會上鎖,這樣其他線程想操作這個資料拿不到鎖隻能阻塞了。
在 Java 語言中 synchronized 和 ReentrantLock等就是典型的悲觀鎖,還有一些使用了 synchronized 關鍵字的容器類如 HashTable 等也是悲觀鎖的應用。
樂觀鎖 對應于生活中樂觀的人,樂觀的人總是想着事情往好的方向發展。
舉個生活中的例子,假設廁所隻有一個坑位了,樂觀鎖認為:這荒郊野外的,又沒有什麼人,不會有人搶我坑位的,每次關門上鎖多浪費時間,還是不加鎖好了。你看樂觀鎖就是天生樂觀!
回到代碼世界中,樂觀鎖操作資料時不會上鎖,在更新的時候會判斷一下在此期間是否有其他線程去更新這個資料。
樂觀鎖可以使用版本号機制和CAS算法實作。在 Java 語言中 java.util.concurrent.atomic包下的原子類就是使用CAS 樂觀鎖實作的。
兩種鎖的使用場景
悲觀鎖和樂觀鎖沒有孰優孰劣,有其各自适應的場景。
樂觀鎖适用于寫比較少(沖突比較小)的場景,因為不用上鎖、釋放鎖,省去了鎖的開銷,進而提升了吞吐量。
如果是寫多讀少的場景,即沖突比較嚴重,線程間競争激勵,使用樂觀鎖就是導緻線程不斷進行重試,這樣可能還降低了性能,這種場景下使用悲觀鎖就比較合适。
獨占鎖是指鎖一次隻能被一個線程所持有。如果一個線程對資料加上排他鎖後,那麼其他線程不能再對該資料加任何類型的鎖。獲得獨占鎖的線程即能讀資料又能修改資料。
JDK中的synchronized和java.util.concurrent(JUC)包中Lock的實作類就是獨占鎖。
共享鎖是指鎖可被多個線程所持有。如果一個線程對資料加上共享鎖後,那麼其他線程隻能對資料再加共享鎖,不能加獨占鎖。獲得共享鎖的線程隻能讀資料,不能修改資料。
在 JDK 中 ReentrantReadWriteLock 就是一種共享鎖。
互斥鎖是獨占鎖的一種正常實作,是指某一資源同時隻允許一個通路者對其進行通路,具有唯一性和排它性。
互斥鎖一次隻能一個線程擁有互斥鎖,其他線程隻有等待。
讀寫鎖是共享鎖的一種具體實作。讀寫鎖管理一組鎖,一個是隻讀的鎖,一個是寫鎖。
讀鎖可以在沒有寫鎖的時候被多個線程同時持有,而寫鎖是獨占的。寫鎖的優先級要高于讀鎖,一個獲得了讀鎖的線程必須能看到前一個釋放的寫鎖所更新的内容。
讀寫鎖相比于互斥鎖并發程度更高,每次隻有一個寫線程,但是同時可以有多個線程并發讀。
在 JDK 中定義了一個讀寫鎖的接口:ReadWriteLock
ReentrantReadWriteLock 實作了ReadWriteLock接口,具體實作這裡不展開,後續會深入源碼解析。
公平鎖是指多個線程按照申請鎖的順序來擷取鎖,這裡類似排隊買票,先來的人先買,後來的人在隊尾排着,這是公平的。
在 java 中可以通過構造函數初始化公平鎖
非公平鎖是指多個線程擷取鎖的順序并不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先擷取鎖,在高并發環境下,有可能造成優先級翻轉,或者饑餓的狀态(某個線程一直得不到鎖)
在 java 中 synchronized 關鍵字是非公平鎖,ReentrantLock預設也是非公平鎖。
可重入鎖又稱之為遞歸鎖,是指同一個線程在外層方法擷取了鎖,在進入内層方法會自動擷取鎖。
對于Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖。對于Synchronized而言,也是一個可重入鎖。
敲黑闆:可重入鎖的一個好處是可一定程度避免死鎖。
以 synchronized 為例,看一下下面的代碼:
上面的代碼中 methodA 調用 methodB,如果一個線程調用methodA 已經擷取了鎖再去調用 methodB 就不需要再次擷取鎖了,這就是可重入鎖的特性。如果不是可重入鎖的話,mehtodB 可能不會被目前線程執行,可能造成死鎖。
自旋鎖是指線程在沒有獲得鎖時不是被直接挂起,而是執行一個忙循環,這個忙循環就是所謂的自旋。
自旋鎖的目的是為了減少線程被挂起的幾率,因為線程的挂起和喚醒也都是耗資源的操作。
如果鎖被另一個線程占用的時間比較長,即使自旋了之後目前線程還是會被挂起,忙循環就會變成浪費系統資源的操作,反而降低了整體性能。是以自旋鎖是不适應鎖占用時間長的并發情況的。
在 Java 中,AtomicInteger 類有自旋的操作,我們看一下代碼:
CAS 操作如果失敗就會一直循環擷取目前 value 值然後重試。
另外自适應自旋鎖也需要了解一下。
在JDK1.6又引入了自适應自旋,這個就比較智能了,自旋時間不再固定,由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀态來決定。如果虛拟機認為這次自旋也很有可能再次成功那就會次序較多的時間,如果自旋很少成功,那以後可能就直接省略掉自旋過程,避免浪費處理器資源。
分段鎖 是一種鎖的設計,并不是具體的一種鎖。
分段鎖設計目的是将鎖的粒度進一步細化,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
在 Java 語言中 CurrentHashMap 底層就用了分段鎖,使用Segment,就可以進行并發使用了。
JDK1.6 為了提升性能減少獲得鎖和釋放鎖所帶來的消耗,引入了4種鎖的狀态:無鎖、偏向鎖、輕量級鎖和重量級鎖,它會随着多線程的競争情況逐漸更新,但不能降級。
無鎖狀态其實就是上面講的樂觀鎖,這裡不再贅述。
Java偏向鎖(Biased Locking)是指它會偏向于第一個通路鎖的線程,如果在運作過程中,隻有一個線程通路加鎖的資源,不存在多線程競争的情況,那麼線程是不需要重複擷取鎖的,這種情況下,就會給線程加一個偏向鎖。
偏向鎖的實作是通過控制對象Mark Word的标志位來實作的,如果目前是可偏向狀态,需要進一步判斷對象頭存儲的線程 ID 是否與目前線程 ID 一緻,如果一緻直接進入。
當線程競争變得比較激烈時,偏向鎖就會更新為輕量級鎖,輕量級鎖認為雖然競争是存在的,但是理想情況下競争的程度很低,通過自旋方式等待上一個線程釋放鎖。
如果線程并發進一步加劇,線程的自旋超過了一定次數,或者一個線程持有鎖,一個線程在自旋,又來了第三個線程通路時(反正就是競争繼續加大了),輕量級鎖就會膨脹為重量級鎖,重量級鎖會使除了此時擁有鎖的線程以外的線程都阻塞。
更新到重量級鎖其實就是互斥鎖了,一個線程拿到鎖,其餘線程都會處于阻塞等待狀态。
在 Java 中,synchronized 關鍵字内部實作原理就是鎖更新的過程:無鎖 --> 偏向鎖 --> 輕量級鎖 --> 重量級鎖。這一過程在後續講解 synchronized 關鍵字的原理時會詳細介紹。
鎖粗化就是将多個同步塊的數量減少,并将單個同步塊的作用範圍擴大,本質上就是将多次上鎖、解鎖的請求合并為一次同步請求。
舉個例子,一個循環體中有一個代碼同步塊,每次循環都會執行加鎖解鎖操作。
經過鎖粗化後就變成下面這個樣子了:
鎖消除是指虛拟機編譯器在運作時檢測到了共享資料沒有競争的鎖,進而将這些鎖進行消除。
舉個例子讓大家更好了解。
上面代碼中有一個 test 方法,主要作用是将字元串 s1 和字元串 s2 串聯起來。
test 方法中三個變量s1, s2, stringBuffer, 它們都是局部變量,局部變量是在棧上的,棧是線程私有的,是以就算有多個線程通路 test 方法也是線程安全的。
我們都知道 StringBuffer 是線程安全的類,append 方法是同步方法,但是 test 方法本來就是線程安全的,為了提升效率,虛拟機幫我們消除了這些同步鎖,這個過程就被稱為鎖消除。
一張圖總結:
前面講了 Java 語言中各種各種的鎖,最後再通過六個問題統一總結一下:
感謝大家的認同與支援,小編會持續轉發《樂位元組》優質文章