樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。
java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到并發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。
java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如 RetreenLock。
自旋鎖
自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
線程自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那線程也不能一直占用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。
自旋鎖的優缺點
自旋鎖盡可能的減少線程的阻塞,這對于鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小于線程阻塞掛起再喚醒的操作的消耗,這些操作會導致線程發生兩次上下文切換!
但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間占用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是占用 cpu 做無用功,占著 XX 不 XX,同時有大量線程在競爭一個鎖,會導致獲取鎖的時間很長,線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要 cup 的線程又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)
自旋鎖的目的是為了占著 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的線程處于自旋狀態占用 CPU 資源,進而會影響整體系統的性能。因此自旋的周期選的額外重要!
JVM 對于自旋周期的選擇,jdk1.5 這個限度是一定的寫死的,在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認為一個線程上下文切換的時間是最佳的一個時間,同時 JVM 還針對當前 CPU 的負荷情況做了較多的優化,如果平均負載小于 CPUs 則一直自旋,如果有超過(CPUs/2)個線程正在自旋,則后來線程直接阻塞,如果正在自旋的線程發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞,如果 CPU 處于節電模式則停止自旋,自旋時間的最壞情況是 CPU的存儲延遲(CPU A 存儲了一個數據,到 CPU B 得知這個數據直接的時間差),自旋時會適當放棄線程優先級之間的差異。
自旋鎖的開啟
JDK1.6 中-XX:+UseSpinning 開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7 后,去掉此參數,由 jvm 控制;
Synchronized 同步鎖
synchronized 它可以把任意一個非 NULL 的對象當作鎖。他屬于獨占式的悲觀鎖,同時屬于可重入鎖。
Synchronized 作用范圍
-
作用于方法時,鎖住的是對象的實例(this);
-
當作用于靜態方法時,鎖住的是Class實例,又因為Class的相關數據存儲在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全局共享的,因此靜態方法鎖相當于類的一個全局鎖,會鎖所有調用該方法的線程;
-
synchronized 作用于一個對象實例時,鎖住的是所有以該對象為鎖的代碼塊。它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。
Synchronized 核心組件
- Wait Set:哪些調用 wait 方法被阻塞的線程被放置在這里;
- Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中;
- Entry List:Contention List 中那些有資格成為候選資源的線程被移動到 Entry List 中;
- OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程被成為 OnDeck;
- Owner:當前已經獲取到所資源的線程被稱為 Owner;
- !Owner:當前釋放鎖的線程。
Synchronized 實現
-
JVM 每次從隊列的尾部取出一個數據用于鎖競爭候選者(OnDeck),但是并發情況下,ContentionList 會被大量的并發線程進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將一部分線程移動到 EntryList 中作為候選競爭線程。
-
Owner 線程會在 unlock 時,將 ContentionList 中的部分線程遷移到 EntryList 中,并指定EntryList 中的某個線程為 OnDeck 線程(一般是最先進去的那個線程)。
-
Owner 線程并不直接把鎖傳遞給 OnDeck 線程,而是把鎖競爭的權利交給 OnDeck,OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM 中,也把這種選擇行為稱之為“競爭切換”。
-
OnDeck 線程獲取到鎖資源后會變為 Owner 線程,而沒有得到鎖資源的仍然停留在 EntryList中。如果 Owner 線程被 wait 方法阻塞,則轉移到 WaitSet 隊列中,直到某個時刻通過 notify或者 notifyAll 喚醒,會重新進去 EntryList 中。
-
處于 ContentionList、EntryList、WaitSet 中的線程都處于阻塞狀態,該阻塞是由操作系統來完成的(Linux 內核下采用 pthread_mutex_lock 內核函數實現的)。
-
Synchronized 是非公平鎖。 Synchronized 在線程進入 ContentionList 時,等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對于已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶占 OnDeck 線程的鎖資源。
-
每個對象都有個 monitor 對象,加鎖就是在競爭 monitor 對象,代碼塊加鎖是在前后分別加上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的
-
synchronized 是一個重量級操作,需要調用操作系統相關接口,性能是低效的,有可能給線程加鎖消耗的時間比有用操作消耗的時間更多。
-
Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之后推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做了優化。引入了偏向鎖和輕量級鎖。都是在對象頭中有標記位,不需要經過操作系統加鎖。
-
鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;
-
JDK 1.6 中默認是開啟偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖。
ReentrantLock
ReentantLock 繼承接口 Lock 并實現了接口中定義的方法,他是一種可重入鎖,除了能完成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。
Lock 接口的主要方法
-
void lock(): 執行此方法時, 如果鎖處于空閑狀態, 當前線程將獲取到鎖. 相反, 如果鎖已經被其他線程持有, 將禁用當前線程, 直到當前線程獲取到鎖.
-
boolean tryLock():如果鎖可用, 則獲取鎖, 并立即返回 true, 否則返回 false. 該方法和lock()的區別在于, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前線程被禁用, 當前線程仍然繼續往下執行代碼. 而 lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當前線程并不繼續向下執行.
-
void unlock():執行此方法時, 當前線程將釋放持有的鎖. 鎖只能由持有者釋放, 如果線程并不持有鎖, 卻執行該方法, 可能導致異常的發生.
-
Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的 await()方法,而調用后,當前線程將縮放鎖。
-
getHoldCount() :查詢當前線程保持此鎖的次數,也就是執行此線程執行 lock 方法的次數。
-
getQueueLength():返回正等待獲取此鎖的線程估計數,比如啟動 10 個線程,1 個線程獲得鎖,此時返回的是 9
-
getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線程估計數。比如 10 個線程,用同一個 condition 對象,并且此時這 10 個線程都執行了condition 對象的 await 方法,那么此時執行此方法返回 10
-
hasWaiters(Condition condition):查詢是否有線程等待與此鎖有關的給定條件(condition),對于指定 contidion 對象,有多少線程執行了 condition.await 方法
-
hasQueuedThread(Thread thread):查詢給定線程是否等待獲取此鎖
-
hasQueuedThreads():是否有線程等待此鎖
-
isFair():該鎖是否公平鎖
-
isHeldByCurrentThread(): 當前線程是否保持鎖鎖定,線程的執行 lock 方法的前后分別是 false 和 true
-
isLock():此鎖是否有任意線程占用
-
lockInterruptibly():如果當前線程未被中斷,獲取鎖
-
tryLock():嘗試獲得鎖,僅在調用時鎖未被線程占用,獲得鎖
-
tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖。
非公平鎖
JVM 按隨機、就近原則分配鎖的機制則稱為不公平鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式,默認為非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。
公平鎖
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。
ReentrantLock 與 synchronized
-
ReentrantLock 通過方法 lock()與 unlock()來進行加鎖與解鎖操作,與 synchronized 會被 JVM 自動解鎖機制不同,ReentrantLock 加鎖后需要手動進行解鎖。為了避免程序出現異常而無法正常解鎖的情況,使用 ReentrantLock 必須在 finally 控制塊中進行解鎖操作。
-
ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。這種情況下需要使用 ReentrantLock。
ReentrantLock 實現
public class MyService {private Lock lock = new ReentrantLock();//Lock lock=new ReentrantLock(true);//公平鎖//Lock lock=new ReentrantLock(false);//非公平鎖private Condition condition = lock.newCondition();//創建 Conditionpublic void testMethod() {try {lock.lock();//lock 加鎖//1:wait 方法等待://System.out.println("開始 wait");condition.await();//通過創建 Condition 對象來使線程 wait,必須先執行 lock.lock 方法獲得鎖//:2:signal 方法喚醒condition.signal();//condition 對象的 signal 方法可以喚醒 wait 線程for (int i = 0; i < 5; i++) {System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}
Condition 類和 Object 類鎖方法區別區別
- Condition 類的 awiat 方法和 Object 類的 wait 方法等效
- Condition 類的 signal 方法和 Object 類的 notify 方法等效
- Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
- ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的
tryLock 和 lock 和 lockInterruptibly 的區別
-
tryLock 能獲得鎖就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false
-
lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖
-
lock 和 lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程,lock 不會拋出異常,而 lockInterruptibly 會拋出異常。
Semaphore 信號量
Semaphore 是一種基于計數的信號量。它可以設定一個閾值,基于此,多個線程競爭獲取許可信號,做完自己的申請后歸還,超過閾值后,線程申請許可信號將會被阻塞。Semaphore 可以用來構建一些對象池,資源池之類的,比如數據庫連接池
實現互斥鎖(計數器為 1)
我們也可以創建計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。
代碼實現
它的用法如下:
// 創建一個計數閾值為 5 的信號量對象
// 只能 5 個線程同時訪問
Semaphore semp = new Semaphore(5);
try { // 申請許可semp.acquire();try {// 業務邏輯} catch (Exception e) {} finally {// 釋放許可semp.release();}
} catch (InterruptedException e) {}
Semaphore 與 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也與之類似,通過 acquire()與release()方法來獲得和釋放臨界資源。經實測,Semaphone.acquire()方法默認為可響應中斷鎖,與 ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock不同,其使用方法與 ReentrantLock 幾乎一致。Semaphore 也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。
Semaphore 的鎖釋放操作也由手動進行,因此與 ReentrantLock 一樣,為避免線程因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 代碼塊中完成。
AtomicInteger
首先說明,此處 AtomicInteger ,一個提供原子操作的 Integer 的類,常見的還有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他們的實現原理相同,區別在與運算對象類型的不同。令人興奮地,還可以通過 AtomicReference將一個對象的所有操作轉化成原子操作。
我們知道,在多線程程序中,諸如++i 或 i++等運算不具有原子性,是不安全的線程操作之一。通常我們會使用 synchronized 將該操作變成一個原子操作,但 JVM 為此類操作特意提供了一些同步類,使得使用更方便,且使程序運行效率變得更高。通過相關資料顯示,通常AtomicInteger的性能是 ReentantLock 的好幾倍。
可重入鎖(遞歸鎖)
本文里面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。
公平鎖與非公平鎖
公平鎖(Fair)
加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
- 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個隊列
- Java 中的 synchronized 是非公平鎖,ReentrantLock 默認的 lock()方法采用的是非公平鎖。
ReadWriteLock 讀寫鎖
為了提高性能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程序的執行效率。讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可。
讀鎖
如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖
寫鎖
如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
Java 中讀寫鎖有個接口 java.util.concurrent.locks.ReadWriteLock ,也有具體的實現ReentrantReadWriteLock。
共享鎖和獨占鎖
java 并發包提供的加鎖模式分為獨占鎖和共享鎖。
獨占鎖
獨占鎖模式下,每次只能有一個線程能持有鎖,ReentrantLock 就是以獨占方式實現的互斥鎖。獨占鎖是一種悲觀保守的加鎖策略,它避免了讀/讀沖突,如果某個只讀線程獲取鎖,則其他讀線程都只能等待,這種情況下就限制了不必要的并發性,因為讀操作并不會影響數據的一致性。
共享鎖
共享鎖則允許多個線程同時獲取鎖,并發訪問 共享資源,如:ReadWriteLock。共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源。
-
AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 隊列中等待線程的鎖獲取模式。
-
java 的并發包中提供了 ReadWriteLock,讀-寫鎖。它允許一個資源可以被多個讀操作訪問,或者被一個 寫操作訪問,但兩者不能同時進行。
重量級鎖(Mutex Lock)
Synchronized 是通過對象內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴于底層的操作系統的 Mutex Lock 來實現的。而操作系統實現線程之間的切換這就需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什么Synchronized 效率低的原因。因此,這種依賴于操作系統 Mutex Lock 所實現的鎖我們稱之為“重量級鎖”。JDK 中對 Synchronized 做的種種優化,其核心都是為了減少這種重量級鎖的使用。
JDK1.6 以后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了“輕量級鎖”和“偏向鎖”。
輕量級鎖
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
鎖升級
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。
“輕量級”是相對于使用操作系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
偏向鎖
Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之后,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換ThreadID 的時候依賴一次 CAS 原子指令(由于一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節省下來的 CAS 原子指令的性能消耗)。上面說過,輕量級鎖是為了在線程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進
一步提高性能。
分段鎖
分段鎖也并非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐
鎖優化
減少鎖持有時間
只用在有線程安全要求的程序上加鎖
減小鎖粒度
將大對象(這個對象可能會被很多線程訪問),拆成小對象,大大增加并行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。最最典型的減小鎖粒度的案例就是ConcurrentHashMap。
鎖分離
最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了線程安全,又提高了性能,具體也請查看[高并發 Java 五] JDK 并發包 1。讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。比如LinkedBlockingQueue 從頭部取出,從尾部放數據
鎖粗化
通常情況下,為了保證多線程間的有效并發,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利于性能的優化 。
鎖消除
鎖消除是在編譯器級別的事情。在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作,多數是因為程序員編碼不規范引起。