面试必会系列 - 1.5 Java 锁机制
本文已收錄至 github,完整圖文:https://github.com/HanquanHq/MD-Notes
面試必會系列專欄:https://blog.csdn.net/sinat_42483341/category_10300357.html
Java 鎖機制
概覽
- syncronized 鎖升級過程
- ReentrantLock 可重入鎖
- volatile 關鍵字
- JUC 包下新的同步機制
syncronized
給一個變量/一段代碼加鎖,線程拿到鎖之后,才能修改一個變量/執行一段代碼
- wait()
- notify()
synchronized 關鍵字可以作用于 方法 或者 代碼塊,最主要有以下幾種使用方式:
注意:
syncronized 實現原理?
Object o = new Object(); synchronized (o) {}添加 synchronized 之后,生成的 .class 字節碼:
0 new #2 <java/lang/Object>3 dup4 invokespecial #1 <java/lang/Object.<init>>7 astore_18 aload_19 dup 10 astore_2 11 monitorenter // 獲取鎖 12 aload_2 13 monitorexit // 釋放鎖 14 goto 22 (+8) 17 astore_3 18 aload_2 19 monitorexit // 兜底:如果發生異常,自動釋放鎖 20 aload_3 21 athrow 22 return1、字節碼層面:ACC_SYNCHRONIZED,monitorenter,monitorexit(重量級鎖)
事實上,只有在JDK1.6之前,synchronized的實現才會直接調用ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。從JDK6開始,HotSpot虛擬機開發團隊對Java中的鎖進行優化,如增加了適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等優化策略。
https://juejin.im/post/6844903918653145102
ACC_SYNCHRONIZED:把 syncronized 加在方法上時的字節碼
方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先持有 monitor(虛擬機規范中用的是管程一詞),然后再執行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor
ACC_SYNCHRONIZED:
方法級別的同步是隱式的,作為方法調用的一部分。同步方法的常量池中會有一個ACC_SYNCHRONIZED標志。當調用一個設置了ACC_SYNCHRONIZED標志的方法,執行線程需要先獲得monitor鎖,然后開始執行方法,方法執行之后再釋放monitor鎖,當方法不管是正常return還是拋出異常都會釋放對應的monitor鎖。在這期間,如果其他線程來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。如果在方法執行過程中,發生了異常,并且方法內部并沒有處理該異常,那么在異常被拋到方法外面之前監視器鎖會被自動釋放。
monitorenter, monitorexit:把 syncronized 用于對象時的字節碼
代碼塊的同步是利用 monitorenter 和 monitorexit 這兩個字節碼指令。它們分別位于同步代碼塊的開始和結束位置。當 JVM 執行到 monitorenter 指令時,當前線程試圖獲取 monitor 對象的所有權。鎖重入的原理:
- 如果未加鎖或者已經被當前線程所持有,把鎖計數器 +1
- 當執行 monitorexit 指令時,鎖計數器 -1
- 當鎖計數器為 0 時,鎖被釋放
- 如果獲取monitor對象失敗,該線程進入阻塞狀態,直到其他線程釋放鎖。
monitorenter:
“它的實現在 hotspot 源碼的 interpreterRuntime.cpp 中,在 monitorenter 函數內部的實現為:如果打開了偏向鎖,則進入 fast_enter, 在 safepoint情況下,嘗試獲取偏向鎖,成功則返回,失敗則進入 slow_enter, 升級為自旋鎖,如果自旋鎖失敗,則膨脹 inflate 成為重量級鎖。重量級鎖的代碼在 syncronizer.cpp 中,里面調用了 linux 內核的一些實現方法。
每個對象都與一個 monitor 相關聯。當且僅當擁有所有者時(被擁有),monitor才會被鎖定。執行到monitorenter指令的線程,會嘗試去獲得對應的 monitor,如下:
每個對象維護著一個記錄著被鎖次數的計數器, 對象未被鎖定時,該計數器為0。線程進入monitor(執行monitorenter 指令)時,會把計數器設置為1。當同一個線程再次獲得該對象的鎖的時候,計數器再次自增,這就是鎖重入。當其他線程想獲得該 monitor 的時候,就會阻塞,直到計數器為0才能成功。
monitorexit:
monitor 的擁有者線程才能執行 monitorexit 指令。線程執行monitorexit指令,就會讓monitor的計數器減一。如果計數器為0,表明該線程不再擁有monitor。其他線程就允許嘗試去獲得該monitor了。
monitor 監視器
monitor 是什么? 它可以理解為一種同步工具,或者說是同步機制,它通常被描述成一個對象。操作系統的管程 是概念原理,在 HotSpot 中,Monitor(管程)是由 ObjectMonitor 實現的。
Java Monitor 的工作機理如圖所示:
對象是如何跟 monitor 關聯的呢?直接看圖:
對象里有對象頭,對象頭里面有 markmord,markmord 指針指向了 monitor
2、JVM 層面
- C, C++ 調用了操作系統提供的同步機制,在 win 和 linux 上不同
3、OS 和硬件層面
- X86 : lock cmpxchg / xxx
- lock 是處理多處理器之間的總線鎖問題
syncronized 鎖升級過程
早期(JDK 1.2 以前)syncronized 都是重量級鎖,向操作系統申請鎖。后來進行了優化,有了鎖升級過程:
偏向鎖、自旋鎖都是用戶空間完成,JVM自己管理;重量級鎖需要向內核申請
markword 組成
偏向鎖
-
普通對象加了 syncronized,會加上偏向鎖。偏向鎖默認是打開的,但是有一個時延,如果要觀察到偏向鎖,應該設定參數
-
**為什么要有偏向鎖?**我們知道,Vector,StringBuffer 都有很多使用了 syncronized 的同步方法,但是在工業實踐中,我們通常是在單線程的時候使用它的,**沒有必要 **設計 鎖競爭機制。為了在沒有競爭的情況下減少鎖開銷,偏向鎖偏向于第一個獲得它的線程,把第一個訪問的 線程 id(在C++實現中叫線程指針) 寫到 markword 中,而不去真正加鎖。如果一直沒有被其他線程競爭,則持有偏向鎖的線程將不需要進行同步。
默認情況,偏向鎖有個時延,默認是4秒。why? 因為 JVM 虛擬機自己有一些默認啟動的線程,里面有好多sync代碼,明確知道這些sync代碼啟動時會有競爭,如果使用偏向鎖,就會造成偏向鎖不斷的進行鎖撤銷和鎖升級的操作,效率較低,所以默認偏向鎖啟動延時 4s。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 // 設置偏向鎖0s立刻啟動設定上述參數,new Object () - > 101 偏向鎖 -> 線程ID為0 -> 匿名偏向 Anonymous BiasedLock ,指還沒有偏向任何一個線程。打開偏向鎖,new出來的對象,默認就是一個可偏向匿名對象 101(或者 sleep 5000 之后再打印也可以看到偏向鎖)
輕量級鎖(也叫 自旋鎖 / 無鎖 / CAS)
-
偏向鎖時,有其他線程來競爭鎖,則先把 偏向鎖撤銷,然后進行 自旋鎖(輕量級鎖)競爭。
-
在沒有競爭的前提下,減少 重量級鎖使用操作系統 mutex 互斥量 產生的性能消耗
JVM虛擬機在每一個競爭線程的棧幀中,建立一個自己的 **鎖記錄 (Lock Record, LR) **空間,存儲鎖對象目前 markword 的拷貝。競爭線程 使用 CAS 的方式,嘗試把被競爭對象的 markword 更新為指向競爭線程 LR 的指針,如果更新成功即代表該線程擁有了鎖,鎖標志位將轉變為 00,表示處于輕量級鎖定狀態。
-
CAS 是一種樂觀鎖:cas(v, a, b) 變量v,期待a,修改值b
-
Java 中調用了 native 的 compareAndSwapXXX() 方法
-
每個人在自己的線程內部生成一個自己LR(Lock Record鎖記錄),兩個線程通過自己的方式嘗試將 LR 寫門上,競爭成功的開始運行,競爭失敗的一直自旋等待。
-
實際上是匯編指令 lock cmpxchg,硬件層面實現:在操作過程中不允許被其他CPU打斷,避免CAS在寫數據的時候被其他線程打斷,相比操作系統級別的鎖,效率要高很多。
LOCK 本身不是一個指令:它是一個指令前綴,該指令必須是對存儲器( INC, XCHG, CMPXCHG等)進行 讀 – 修改 – 寫操作的指令,在這種情況下,它是在所保存的地址處包含字的incl (%ecx)指令在ecx寄存器中。
LOCK 前綴確保CPU在操作期間擁有適當的caching行的獨占所有權,并提供某些額外的訂購保證。 這可以通過聲明一個總線鎖來實現,但是CPU將盡可能地避免這種情況。
CPU在執行cmpxchg指令之前會執行lock鎖定總線,實際是鎖定北橋信號。現在的主板貌似沒有南北橋了,集成到cpu里面了 http://www.360doc.com/content/18/0216/10/44130189_730197818.shtml
-
如何解決ABA問題?
- 基礎數據類型即使出現了ABA,一般問題不大。
- 解決方式:加版本號,后面檢查的時候連版本號一起檢查。
- Atomic里面有個帶版本號的類 AtomicStampedReference,目前還沒有人在面試的時候遇到過。
-
線程始終得不到鎖會自旋消耗 CPU
重量級鎖
- 輕量級鎖再競爭,升級為重量級鎖
- 重量級鎖向 Linux 內核 申請鎖 mutex, CPU從3級-0級系統調用,線程掛起,進入等待隊列,等待操作系統的調度,然后再映射回用戶空間。重量級鎖有一個 waitset 等待隊列,不需要 CAS 消耗 CPU 時間。由操作系統的 CFS 公平調度策略來調度。
- 在 markword 中記錄 ObjectMonitor,是 JVM 用 C++ 寫的一個 Object,需要向操作系統申請鎖,詳細過程你去讀 HotSpot 的 interpreterRuntime.cpp 的 monitorenter方法
自旋鎖,什么時候升級為重量級鎖?
競爭加劇:有線程超過10次自旋, -XX:PreBlockSpin, 或者自旋線程數超過CPU核數的一半, 1.6之后,加入自適應自旋 Adapative Self Spinning , JVM自己控制自旋次數,不需要你設置參數了。所以你在做實驗的時候,會發現有時候 syncronized 并不比 AtomicInteger 效率低。
升級重量級鎖:-> 向操作系統申請資源,linux mutex , CPU從3級-0級系統調用,線程掛起,進入等待隊列,等待操作系統的調度,然后再映射回用戶空間
為什么有自旋鎖,還需要重量級鎖?
自旋是消耗CPU資源的,如果鎖的時間長,或者自旋線程多,CPU會被大量消耗
重量級鎖有等待隊列,所有拿不到鎖的進入等待隊列,不需要消耗CPU資源
偏向鎖,是否一定比自旋鎖效率高?
不一定,在明確知道會有多線程競爭的情況下,偏向鎖肯定會涉及鎖撤銷,這時候直接使用自旋鎖
例如,JVM 啟動過程,會有很多線程競爭(明確知道,比如在剛啟動的之后,肯定有很多線程要爭搶內存的位置),所以,默認情況啟動時不打開偏向鎖,過一段兒時間再打開。
鎖重入
sychronized是可重入鎖
重入次數必須記錄,因為要解鎖幾次必須得對應
偏向鎖、自旋鎖,重入次數存放在線程棧,讓 LR + 1
重量級鎖 -> ? ObjectMonitor 字段上
如果計算過對象的 hashCode,則對象無法進入偏向狀態!
輕量級鎖重量級鎖的hashCode存在與什么地方?
答案:線程棧中,輕量級鎖的LR中,或是代表重量級鎖的ObjectMonitor的成員中
ReentrantLock 可重入鎖
ReentrantLock 的使用
ReentrantLock 和 synchronized 都是可重入鎖,Reentrantlock 可以完成 synchronized 同樣的功能:由于 m1 鎖定 this,只有 m1 執行完畢的時候,m2 才能執行。
-
使用 Reentrantlock,可以進行 tryLock “嘗試鎖定”,這樣如果無法鎖定,或者在指定時間內無法鎖定,線程可以決定是否繼續等待。
-
使用 ReentrantLock 還可以調用 lockInterruptibly 方法,可以對線程 interrupt 方法做出響應,在一個線程等待鎖的過程中,可以被打斷。
-
ReentrantLock 還可以指定為公平鎖,但是效率偏低。
-
需要注意的是,使用 syncronized 鎖定的話,如果遇到異常,JVM 會自動釋放鎖,但是 ReentrantLock 必須 手動釋放鎖,因此經常在 finally 中保證鎖的 unlock 釋放。
https://blog.csdn.net/fuyuwei2015/article/details/83719444 ReentrantLock 原理
public class TestLock {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newCachedThreadPool(); // 線程池ReentrantLock reentrantLock = new ReentrantLock();int count[] = {0};for (int i = 0; i < 10000; i++) {executorService.submit(() -> {try {reentrantLock.lock(); // 獲取鎖count[0]++;} catch (Exception e) {e.printStackTrace();} finally {reentrantLock.unlock(); // 釋放鎖}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.HOURS);System.out.println(count[0]); // 10000} }private Lock lock = new ReentrantLock()
- lock.lock() 獲取鎖
- lock.unlock() 釋放鎖
ReentrantLock 主要利用 CAS + AQS(AbstractQueuedSynchronizer) 來實現。
lock() 與 unlock() 實現原理
可重入鎖
可重入鎖是指,同一個線程可以多次獲取同一把鎖。ReentrantLock 和 synchronized 都是可重入鎖。
可中斷鎖
可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。
公平鎖與非公平鎖
公平鎖是指,多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到順序,非公平鎖則允許線程“插隊”。
synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,但是也可以設置為公平鎖。
ReentrantLock提供了兩個構造器,可以指定使用公平鎖和非公平鎖。分別是:
public ReentrantLock() {sync = new NonfairSync(); }public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }默認初始化為 NonfairSync 對象,即非公平鎖。由 lock() 和 unlock() 的源碼可以看到,它們只是分別調用了 sync.acquire(1); 和 sync.release(1); 方法。
Test.java
reentrantLock.lock();ReentrantLock.java
private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {...}public void lock() {sync.acquire(1); }AbstractQueuedSynchronizer.java
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); }CAS 操作 (CompareAndSwap)
CAS操作簡單的說就是比較并交換。CAS 操作包含三個操作數:內存位置(V)、預期原值(A)、新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。
CAS 有效地說明了:我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。
Java并發包 java.util.concurrent 中大量使用了 CAS 操作,涉及到并發的地方都調用了 sun.misc.Unsafe 類方法進行CAS操作。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Z66yxdnx-1604373776698)(images/image-20200806110142620.png)]
syncronized 和 ReentrantLock 使用哪個?怎么選擇?
要根據場景選擇,因為 syncronized 的鎖升級是不可逆的,所以如果在一個系統中,某一時刻的訪問量比較大的話,升級為重量級鎖,并且不能撤銷,這樣在普通流量下,效率會變差。所以如果你的 QPS 比較穩定的話,推薦使用 syncronized,你不需要去手動加鎖、釋放鎖。
volatile
volatile 作用
一個線程中的改變,在另一個線程中可以立刻看到。
- 保證線程的可見性(但是不能保證原子性,原子性需要加 syncronized)
- 禁止指令的重排序
什么是指令重排序?
為了提高性能,編譯器和處理器通常會對指令進行重排序,重排序指從源代碼到指令序列的重排序,分為三種:
① 編譯器優化的重排序,編譯器 在不改變單線程程序語義的前提下可以重排語句的執行順序。
② 指令級并行的重排序,如果不存在數據依賴性,處理器 可以改變語句對應機器指令的執行順序。
③ 內存系統的重排序。
DCL 單例要不要加 volitile?
需要。為了防止指令重排序導致拿到 半初始化 的變量。只有在超高并發的時候才有可能測出來。實際上我們要單例的時候,通常直接交由 spring 去管理。
單例模式代碼
public class SingleInstance {private SingleInstance() {}private static SingleInstance INSTANCE;public static SingleInstance getInstance() {if (INSTANCE == null) {synchronized (SingleInstance.class) {if (INSTANCE == null) { // Double Check LockINSTANCE = new SingleInstance();}}}return INSTANCE;} }new 對象過程的字節碼
使用 INSTANCE = new SingleInstance() 單條語句創建實例對象時,編譯后形成的指令,并不是一個原子操作,可能被切換到另外的線程打斷。它是分三步來完成的:
0 new #2 <T> // 申請內存 3 dup 4 invokespecial #3 <T.<init>> // 構造方法進行初始化,成員變量賦【默認值】 7 astore_1 // 成員變量賦【初始值】 8 returnJVM 為了優化指令,允許指令重排序,有可能按照 1 –> 3 –> 2 步驟來執行。當線程 a 執行步驟 3 完畢,在執行步驟 2 之前,被切換到線程 b 上,這時候 INSTANCE 判斷為非空,此時線程 b 直接來到 return instance 語句,拿走 INSTANCE 然后使用,導致拿到半初始化的變量。
硬件和 JVM 如何保證特定情況下不亂序?
https://www.jianshu.com/p/64240319ed60 一文解決內存屏障
1、硬件 CPU 層面(針對 x86 CPU)
- sfence(store fence)指令:在 sfence 指令前的寫操作,必須在 sfence 指令后的寫操作前完成。
- lfence(load fence)指令:在 lfence 指令前的讀操作,必須在 lfence 指令后的讀操作前完成。
- mfence(mixed fence)指令:讀寫屏障,mfence指令實現了Full Barrier,相當于StoreLoad Barriers
原子指令,如x86上的lock … 指令是一個 Full Barrier,執行時會鎖住內存子系統來確保執行順序,甚至跨多個CPU。Software Locks 通常使用了內存屏障或原子指令來實現變量可見性和保持程序順序.
2、JVM 層面(JSR133)
- LoadLoad屏障
- Load語句1; LoadLoad屏障; Load語句2
- 在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
- StoreStore屏障
- Store語句1; StoreStore屏障; Store語句2
- 在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- LoadStore屏障
- Load語句1; LoadStore屏障; Store語句2
- 在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
- StoreLoad屏障
- Store語句1; StoreLoad屏障; Load語句2
- 在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
volitile 的實現原理?
1、Java 字節碼層面
- ACC_VOLATILE
2、JVM 層面
規定了一系列 happens-before 原則
對于 volatile 內存區的讀寫,寫操作和讀操作前后都加了屏障
StoreStoreBarrier
volatile 寫操作
StoreLoadBarrier
LoadLoadBarrier
volatile 讀操作
LoadStoreBarrier
3、OS 和硬件層面
使用 volatile 變量進行寫操作,匯編指令帶有 lock 前綴,相當于一個內存屏障,后面的指令不能重排到內存屏障之前。
使用 lock 前綴,引發兩件事:
① 將當前處理器緩存行的數據寫回系統內存。
②使其他處理器的緩存無效。
相當于對緩存變量做了一次 store 和 write 操作,讓 volatile 變量的修改對其他處理器立即可見。
-
windows lock 指令實現
-
MESI 緩存一致性協議實現
什么是 happens-before 8條原則?
JVM 規定,重排序必須遵守的規則——“先行發生原則”,由具體的JVM實現。
對于會改變結果的重排序, JMM 要求編譯器和處理器必須禁止。
對于不會改變結果的重排序,JMM 不做要求。
**程序次序規則:**一個線程內寫在前面的操作先行發生于后面的。
管程鎖定規則: unlock 操作先行發生于后面對同一個鎖的 lock 操作。
**volatile 規則:**對 volatile 變量的寫操作先行發生于后面的讀操作。
**線程啟動規則:**線程的 start 方法先行發生于線程的每個動作。
**線程終止規則:**線程中所有操作先行發生于對線程的終止檢測。
**對象終結規則:**對象的初始化先行發生于 finalize 方法。
**傳遞性:**如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那么操作 A 先行發生于操作 C 。
什么是 as-if-serial?
不管如何重排序,單線程執行結果不會改變,看起來像是串行的一樣。編譯器和處理器必須遵循 as-if-serial 語義。
為了遵循 as-if-serial,編譯器和處理器不會對存在數據依賴關系的操作重排序,因為這種重排序會改變執行結果。但是如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。
as-if-serial 保證單線程程序的執行結果不變,happens-before 保證正確同步的多線程程序的執行結果不變。這兩種語義的目的,都是為了在不改變程序執行結果的前提下盡可能提高程序執行并行度。
JUC包下新的同步機制
CAS
Compare And Swap (Compare And Exchange) / 自旋 / 自旋鎖 / 無鎖 (無重量鎖)
因為經常配合循環操作,直到完成為止,所以泛指一類操作
cas(v, a, b) ,變量v,期待值a, 修改值b
CAS 存在的 ABA 問題
ABA問題:你的女朋友在離開你的這段兒時間經歷了別的人。自旋就是你空轉等待,一直等到她接納你為止。
ABA 問題的解決方式:加版本號(數值型 / bool 型)
- 基礎數據類型即使出現了ABA,一般問題不大。
- 解決方式:加版本號(數值/bool類型)每改變一次,版本號+1;后面檢查的時候連版本號一起檢查。
- Atomic里面有個帶版本號的類 AtomicStampedReference,目前還沒有人在面試的時候遇到過。
Atomic 包里的類基本都是使用 Unsafe 實現的,Unsafe 只提供三種 CAS 方法:compareAndSwapInt、compareAndSwapLong、compareAndSwapObject,例如,原子更新 Boolean 是先轉成整形再使用 compareAndSwapInt
AtomicInteger 原理
incrementAndGet() 方法,不用你加鎖,也能實現以原子方式將當前的值加 1,它的實現原理:
在 for 死循環中取得 AtomicInteger 里存儲的數值
對 AtomicInteger 當前的值加 1
調用 compareAndSet 方法進行原子更新,先檢查當前數值是否等于 expect,如果等于則說明當前值沒有被其他線程修改,則將值更新為 next,否則會更新失敗返回 false,程序會進入 for 循環重新進行 compareAndSet 操作。
源碼級別的實現原理:
-
getAndIncrement()調用 Unsafe.class 類 getAndAddInt(...)
-
getAndAddInt(...) 調用 this.compareAndSwapInt(...), native 方法, hotspot cpp 實現
-
這個方法在 unsafe.cpp 中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj);jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 注意這里 cmpxchg UNSAFE_END -
cmpxchg 在 atomic.cpp 中,里面調用了另外一個 cmpxchg ,到最后你會來到 atomic_linux_x86.inline.hpp , 93行 cmpxchg ,用內聯匯編的方式實現。
// atomic_linux_x86.inline.hpp inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {int mp = os::is_MP(); // is_MP = Multi Processor,如果是多處理器,則在前面加lock指令__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory");return exchange_value; }jdk8u: atomic_linux_x86.inline.hpp
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "最終實現:cmpxchg ,相當于使用 CAS 的方法修改變量值,這個在 CPU 級別是有原語支持的。
lock cmpxchg // 這個指令,在執行這條指令的過程中,是不允許被其他線程打斷的! // 硬件層面的實現,是鎖北橋信號,雖然也是加鎖了,但是這個鎖比操作系統級別、比JVM級別的鎖的效率會高跟多
JUC 包下的一些用于同步的類
-
AtomicInteger,上面講了
-
AtomicLong
-
ReentrantLock
- 可重入鎖
- 必須要finally中手動釋放鎖
- 可以指定為公平鎖
-
LongAdder
- LongAdder內部做了一個類似于分段鎖,最終將每一個向上遞增的結果加到一起,比 AtomicXXX 快
-
CountDownLatch
- 門栓,每次調用 countDown 方法時計數器減 1,await 方法會阻塞當前線程直到計數器變為0
- 和 Join 的對比:CountDownLatch可以更靈活,因為在一個線程中,CountDownLatch可以根據你的需要countDown很多次。而 join 是等待所有 join 進來的線程結束之后,才繼續執行被 join 的線程。
-
CyclicBarrier
- 循環柵欄:這里有一個柵欄,什么時候人滿了,就把柵欄推倒,嘩啦嘩啦的都放出去,出去之后,柵欄又重新起來,再來人,滿了推倒,以此類推。
- 使一個線程等待其他線程各自執行完畢后再執行。是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢后,計數器的值就-1,當計數器的值為0時,表示所有線程都執行完畢,然后在閉鎖上等待的線程就可以恢復工作了。
- 適用于多線程計算數據,最后合并計算結果的應用場景。
-
Phaser
- 按照不同的階段來對線程進行執行
- 場景:n個人全到場才能吃飯,全吃完才能離開,全離開才能打掃
-
ReadWriteLock
- 讀寫鎖,其實就是 shared 共享鎖 和 exclusive 排他鎖
- 讀寫有很多種情況,比如,你數據庫里的某條數據,你放在內存里讀的時候特別多,你改的次數并不多。這時候將讀寫的鎖分開,會大大提高效率,因為讀操作本質上是可以允許多個線程同時進行的。
-
Semaphore
-
信號量,類似于令牌桶,用來控制同時訪問特定資源的線程數量,通過協調各個線程以保證合理使用公共資源。信號量可以用于流量控制,特別是公共資源有限的應用場景,比如數據庫連接。
-
可以用于限流:最多允許多少個 線程同時在運行
-
Semaphore 的構造方法參數接收一個 int 值,表示可用的許可數量即最大并發數。
使用 acquire 方法獲得一個許可證,使用 release 方法歸還許可,用 tryAcquire 嘗試獲得許可
-
-
Exchanger
- 可以想象 exchanger 是一個容器,用來在兩個線程之間交換變量,用于線程間協作
- 兩個線程通過 exchange 方法交換數據,第一個線程執行 exchange 方法后會阻塞等待第二個線程執行該方法,當兩個線程都到達同步點時這兩個線程就可以交換數據,將本線程生產出的數據傳遞給對方。應用場景包括遺傳算法、校對工作等。
-
LockSupport
- 在線程中調用LockSupport.park(),阻塞當前線程
- LockSupport.unpark(t) 喚醒 t 線程
- unpark 方法可以先于 park 方法執行,unpark 依然有效
- 這兩個方法的實現是由 Unsafe 類提供的,原理是操作線程的一個變量在0,1之間切換,控制阻塞和喚醒
- AQS 就是調用這兩個方法進行線程的阻塞和喚醒的。
AQS 原理(AbstractQueuedSyncronizer,抽象的隊列式同步器)
是一個用于構建鎖和同步容器的框架,AQS解決了在實現同步容器時設計的大量細節問題。事實上,concurrent 包內許多類都是基于 AQS 構建的。
如 ReentrantLock, Semaphore, CountDownLatch, CyclicBarrier 等并發類均是基于 AQS 實現的,具體用法:通過繼承 AQS 實現其模板方法,然后將子類作為同步組件的內部類。
ReentrantLock的基本實現,可以概括為:
先通過CAS嘗試獲取鎖。如果此時已經有線程占據了鎖,那就加入AQS隊列并且被掛起。當鎖被釋放之后,排在CLH 隊列隊首的線程會被喚醒,然后CAS再次嘗試獲取鎖。在這個時候,如果:
-
非公平鎖:如果同時還有另一個線程進來嘗試獲取,那么有可能會讓這個線程搶先獲取;
-
公平鎖:如果同時還有另一個線程進來嘗試獲取,當它發現自己不是在隊首的話,就會排到隊尾,由隊首的線程獲取到鎖。
它使用一個 volatile int state 變量作為共享資源。每當有新線程請求資源時,都會進入一個 FIFO 等待隊列,只有當持有鎖的線程釋放鎖資源后該線程才能持有資源。
等待隊列表示 排隊等待鎖的線程,通過 雙向鏈表 實現,線程被封裝在鏈表的 Node 節點中。隊頭節點稱作“哨兵節點”或者“啞節點”,它不與任何線程關聯。其他節點與等待線程關聯,每個節點維護一個等待狀態 waitStatus。
Node 的等待狀態包括:
AQS 的底層是 CAS + volitile,用 CAS 替代了鎖整個鏈表的操作
VarHandle 類
Varhandle 為 java 9 新加功能,用來代替 Unsafe 供開發者使用。
相當于引用,可以指向任何對象或者對象里的某個屬性,相當于可以直接操作二進制碼,效率上比反射高,并封裝有compareAndSet,getAndSet等方法,可以原子性地修改所指對象的值。比如對long的原子性賦值可以使用VarHandle
- 普通屬性也可以進行原子操作
- 比反射快,直接操作二進制碼
總結
以上是生活随笔為你收集整理的面试必会系列 - 1.5 Java 锁机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试必会系列 - 1.4 类加载机制
- 下一篇: 线性代数:特征值有重根时,相同特征值对应