ps:這連天看鎖看了好多資料,資料也不分前后順序,理順概念,搞定關聯脈絡著是廢了一番勁,總算是基本搞清楚了,真是不容易啊,這個時刻我想起一句話:越往深里學,越得看書,權威書籍的資料更全面,連貫
老規矩,妹子鎮樓,撫慰心靈
鎖涉及到的點
鎖涉及到的點很多,這里從底層向上列舉出來:
AQS(抽象隊列同步器)、非阻塞數據結構和原子變量類等基礎類都是基于 volatile 變量的讀/寫和 CAS 實現,是 Java 并發包里實現鎖、同步的一個重要的基礎框架,而像 Lock 、同步器、阻塞隊列、Executor 和并發容器等高層類又是基于基礎類實現
其實每個部分都可以講很多,這里列出來,大家心里有個數
java 線程阻塞的代價
- 如果要阻塞或喚醒一個線程就需要操作系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,因為用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束后切換回用戶態繼續工作
- 如果線程狀態切換是一個高頻操作時,這將會消耗很多CPU處理時間
- 如果對于那些需要同步的簡單的代碼塊,獲取鎖掛起操作消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然非常糟糕的
- 另外線程的切換還涉及到線程自身的上下文切換,原理和戶態 -> 核心態切換一樣
Synchronize 原理
簡單來說 Synchronize 是通過 JVM 的對象監視器 Monitor 進入和退出命令來實現對方法、同步塊的同步的,在進入同步方法調用前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令,本質就是獲取一個 Monitor,而這個獲取過程具有排他性從而達到了同一時刻只能一個線程訪問的目的,而對于沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit 之后才能嘗試繼續獲取鎖。
然后我們來看看線程在進入同步隊列之后是怎么競爭資源的
別的不用關系,我們熟悉 Contention List 、EntryList 就行
- 首先競爭鎖的線程第一次會加入到 Contention List 這個隊列里
- 然后在 Owner unlock 也就是當前線程完事釋放鎖之后,從 Contention List 里面挑選由資格競爭鎖的線程放到 EntryList 里面來
- EntryList 里面所有的線程被喚醒,做一次非公平的 CAS 競爭
- WaitSet 里面放的是被 obj.wait 的線程,只有 notify 、nitifyAll 時才會重新參與競爭
- 處于 ContentionList、EntryList、WaitSet 中的線程都處于阻塞狀態,每次喚醒 EntryList 里面的線程是很大的性能開銷
- 更坑爹的是 Synchronized 是非公平鎖,外面等待的線程會先嘗試自旋獲取鎖,如果獲取不到才進入ContentionList,這明顯對于已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接獲得鎖資源
鎖的特性
這里介紹 java 中并發核心鎖的特性,不是具體的鎖,具體的鎖一般都是多種特性符合存在的,但是的確有些鎖只具有一種特性,但是這種鎖一般都是有專門應用場景的,不具有普遍適用性
樂觀鎖 / 悲觀鎖
樂觀鎖 - 既無鎖機制,好比做人,這個人很樂觀,每次訪問數據的時候都認為別人不會修改,也就不加鎖。適用于多并發讀的場景,CAS算法就是最典型的樂觀鎖
悲觀鎖 - 看名字自然也知道悲觀鎖是樂觀鎖的對立面,悲觀鎖在獲取資源時不管有沒有來競爭,都先加一把鎖,這也是并發中最常用也普通的做法,保險不會出錯,但是缺點是性能不高,使用于多并發寫的場景
公平鎖 / 非公平鎖
公平鎖 - 這個也好理解,還是好比做人,公平鎖做人很規矩,在希望申請資源時,先看看有沒有其他線程排隊等著,有的話就去后面排隊去。缺點同樣是性能低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大
非公平鎖 - 非公平鎖自然也是公平鎖對立的,非公平鎖做人就不地道了,這丫的不排隊,非公平鎖來了才不管你有沒有排隊等著,直接競爭資源,沒競爭才取排隊。優點是比公平鎖性能好些,新的線程有可能直接獲取到 cpu 資源從而減少喚醒阻塞線程的開銷
典型應用 - 就是 ReentrantLock 這個鎖,ReentrantLock(true) 構造參數有一個 boolean 值,true = 公平鎖 ,false = 非公平鎖(默認)
獨享鎖 / 共享鎖
- 獨享鎖 - 鎖只能被一個線程持有, 絕大部分鎖都是獨享鎖,比如我們常用的 synchronized、ReentrantLock 都是獨享鎖
- 共享鎖 - 鎖可以被多個線程持有,但是共享鎖的應用非常狹窄,比如 ReadWriteLock 就是共享鎖, 其寫鎖是獨享鎖,讀鎖是共享鎖
分段鎖
- 分段鎖 - 是特定領域的鎖,一般數據結構上多見,比如 ConcurrentHashMap 用的就是這種鎖。分段鎖核心思路細化鎖的粒度,把整體分成多段,每段有一把鎖,這樣鎖住的只是一段,不影響其他段的讀寫。在 ConcurrentHashMap 中就是給每個分段鏈表加一把鎖,這樣可以大大提高 map 在多線程下整體的讀寫效率。但是在統計 size 的時候,可就是獲取 hashmap 全局信息的時候,就需要獲取所有的分段鎖才能統計
可重入鎖
- 可重入鎖 - 指的是同一個方法若是已經獲取了鎖,但是在方法內還有地方需要這個鎖的話,可以不受鎖的影響,無阻礙的獲得鎖,這點在遞歸函數中是至關重要的,所以也叫可重入鎖遞歸鎖。ReentrantLock、synchronized 都是可重入鎖,但是自旋鎖就不是可重入鎖
自旋鎖
自旋鎖是互斥鎖的進步,自旋鎖在有其他線程競爭同步代碼時,我們可以讓后面請求鎖的那個線程“稍等一會”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖,為了讓線程等待,我們只須讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。這種優化思路就是基于前面說的鎖阻塞的時間總是很短的考量
自旋鎖在 JDK 1.4 中就已經引入,只不過默認是關閉的,在 JDK 1.6 中就已經改為默認開啟了,但是自旋鎖也有自身的局限
java 中鎖的 API
- synchronized - 由 JVM 實現的內置鎖,其特征是:非公平,悲觀,獨享,互斥,可重入鎖,在 JDL 1.6 對其優化之后,可以根據競爭激烈程度,從 無鎖 -> 偏向鎖 -> 輕量級所 -> 重量級鎖升級
- ReentrantLock - 基于 AQS 開發的可重入鎖,自旋鎖,由 JDK 實現,有公平和非公平之分,也可以感知到中斷。ReenTrantLock的自旋,是通過循環調用 CAS 操作來實現加鎖的,它的性能好是因為避免了使線程進入內核態的阻塞狀態
- Condition - 條件鎖,用在阻塞隊列中,可控制消費者和生產者的讀取進度
- ReentrantReadWriteLock - 讀寫鎖,讀和寫拆開,可以極大的提高讀多寫少的場景下的性能
- CyclicBarrier - 同樣是基于 AQS 開發的鎖,也叫柵欄鎖,有換代的操作,可重置(想象成多個柵欄一般)
- CountDownLatch - 基于AQS開發的鎖,也叫計時器鎖,當計數器減為0后,就可以執行其他任務了,不可重置
- - 3.Semaphore
- AtomicInteger -
上述兩種鎖機制類型都是“互斥鎖”,學過操作系統的都知道,互斥是進程同步關系的一種特殊情況,相當于只存在一個臨界資源,因此同時最多只能給一個線程提供服務。但是,在實際復雜的多線程應用程序中,可能存在多個臨界資源,這時候我們可以借助Semaphore信號量來完成多個臨界資源的訪問。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也與之類似,通過acquire()與release()方法來獲得和釋放臨界資源。
經實測,Semaphone.acquire()方法默認為可響應中斷鎖,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名tryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。
Semaphore的鎖釋放操作也由手動進行,因此與ReentrantLock一樣,為避免線程因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在finally代碼塊中完成
性能對比:
- synchronized 經過 JDK 1.6 的優化,在低并發時 ReentrantLock 和 synchronized 性能相差無幾,但在高并發時 synchronized 性能會迅速下降幾十倍,而 ReentrantLock 的性能卻能依然維持一個水準,因為 ReentrantLock 采取的是循環調用 CAS 避免了線程切換到阻塞,所以在高并發時建議使用ReentrantLock
- 非公平鎖實際執行效率要遠遠超出公平鎖,一般推薦使用非公平鎖
- 在高并發時 Atomic 性能會優于 ReentrantLock 一倍左右,但是 Atomic 在一段同步代碼中只能出現一個 Atomic 的變量,多于一個同步無效,因為他不能在多個Atomic之間同步
鎖使用策略:
一般來說同步我們優先考慮 synchronized ,synchronized 是 JVM 層面的,自動加鎖取消鎖,不會出 bug,如果有特殊需要再進一步優化。ReentrantLock 和 Atomic 如果用的不好,不僅不能提高性能,還可能帶來災難