面試補習系列:java
樂觀鎖就是樂觀的認爲不會發生衝突,用cas和版本號實現 悲觀鎖就是認爲必定會發生衝突,對操做上鎖node
悲觀鎖,老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。面試
傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
再好比 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
複製代碼
適用場景:數據庫
比較適合寫入操做比較頻繁的場景,若是出現大量的讀取操做,每次讀取的時候都會進行加鎖,這樣會增長大量的鎖的開銷,下降了系統的吞吐量。數組
實現方式: synchronized
和Lock
安全
每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制bash
ABA問題(JDK1.5以後已有解決方案):CAS須要在操做值的時候檢查內存值是否發生變化,沒有發生變化纔會更新內存值。可是若是內存值原來是A,後來變成了B,而後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,可是其實是有變化的。ABA問題的解決思路就是在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從「A-B-A」變成了「1A-2B-3A」。
循環時間長開銷大:CAS操做若是長時間不成功,會致使其一直自旋,給CPU帶來很是大的開銷。
只能保證一個共享變量的原子操做(JDK1.5以後已有解決方案):對一個共享變量執行操做時,CAS可以保證原子操做,可是對多個共享變量操做時,CAS是沒法保證操做的原子性的。
複製代碼
適用場景:markdown
比較適合讀取操做比較頻繁的場景,若是出現大量的寫入操做,數據發生衝突的可能性就會增大,爲了保證數據的一致性,應用層須要不斷的從新獲取數據,這樣會增長大量的查詢操做,下降了系統的吞吐量。數據結構
實現方式:多線程
一、使用版本標識來肯定讀到的數據與提交時的數據是否一致。提交後修改版本標識,不一致時能夠採起丟棄和再次嘗試的策略。
二、Java 中的 Compare and Swap 即 CAS ,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。
三、在 Java 中 java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。
公平鎖:
指多個線程按照申請鎖的順序來獲取鎖。
複製代碼
非公平鎖:
指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。
有可能,會形成優先級反轉或者飢餓現象。
複製代碼
拓展線程飢餓
:
一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態 致使沒法獲取的緣由: 線程優先級較低,沒辦法獲取cpu時間 其餘線程老是能在它以前持續地對該同步塊進行訪問。 線程在等待一個自己也處於永久等待完成的對象(好比調用這個對象的 wait 方法),由於其餘線程老是被持續地得到喚醒。 複製代碼
實現方式: ReenTrantLock
(公平/非公平)
對於Java ReentrantLock
而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。
對於Synchronized
而言,也是一種非公平鎖。因爲其並不像ReentrantLock
是經過AQS(AbstractQueuedSynchronizer)
的來實現線程調度,因此並無任何辦法使其變成公平鎖。
若是一個線程得到過該鎖,能夠再次得到,主要是用途就是在遞歸方面,還有就是防止死鎖,好比在一個同步方法塊中調用了另外一個相同鎖對象的同步方法塊
實現方式: synchronized
、ReentrantLock
獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
複製代碼
實現方式: 獨享鎖: ReentrantLock
和 synchronized
貢獻鎖: ReadWriteLock
拓展:
互斥鎖/讀寫鎖 就是對上面的一種具體實現:
互斥鎖:在Java中的具體實現就是ReentrantLock,synchronized
讀寫鎖:在Java中的具體實現就是ReadWriteLock
複製代碼
對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。對於Synchronized而言,固然是獨享鎖
基於 jdk 1.6 以上
偏向鎖
指的是當前只有這個線程得到,沒有發生爭搶,此時將方法頭的markword設置成0,而後每次過來都cas一下就好,不用重複的獲取鎖.指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價
輕量級鎖
:在偏向鎖的基礎上,有線程來爭搶,此時膨脹爲輕量級鎖,多個線程獲取鎖時用cas自旋獲取,而不是阻塞狀態
重量級鎖
:輕量級鎖自旋必定次數後,膨脹爲重量級鎖,其餘線程阻塞,當獲取鎖線程釋放鎖後喚醒其餘線程。(線程阻塞和喚醒比上下文切換的時間影響大的多,涉及到用戶態和內核態的切換)
實現方式: synchronized
在1.7的concurrenthashmap中有分段鎖的實現,具體爲默認16個的segement數組,其中segement繼承自reentranklock,每一個線程過來獲取一個鎖,而後操做這個鎖下連着的map。
實現方式:
咱們以ConcurrentHashMap來講一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,
它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,
而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
可是,在統計size的時候,可就是獲取hashmap全局信息的時候,就須要獲取全部的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。
複製代碼
synchronized 關鍵字經過一對字節碼指令 monitorenter/monitorexit 實現
前置知識:
對象頭:
Hotspot 虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。其中:
Klass Point 是是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
Mark Word 用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵,因此下面將重點闡述 Mark Word 。
Monitor:
每個 Java 對象都有成爲Monitor 的潛質,由於在 Java 的設計中 ,每個 Java 對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者 Monitor 鎖
複製代碼
對象頭結構:
Monitor數據結構:
ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } 參考: https://blog.csdn.net/javazejian/article/details/72828483 複製代碼
ObjectMonitor中有兩個隊列,_WaitSet
和 _EntryList
,用來保存ObjectWaiter
對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner
指向持有ObjectMonitor
對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList
集合,當線程獲取到對象的monitor
後進入 _Owner
區域並把monitor
中的owner
變量設置爲當前線程同時monitor中的計數器count加1
.
若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)
這裏比較複雜,可是建議仔細閱讀,便於後續分析的時候理解
複製代碼
public class SynchronizedTest { public void test2() { synchronized(this) { } } } 複製代碼
synchronized
關鍵字基於上述兩個指令實現了鎖的獲取和釋放過程:
monitorenter
指令插入到同步代碼塊的開始位置,
monitorexit
指令插入到同步代碼塊的結束位置.
線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 Monitor 全部權,即嘗試獲取對象的鎖。
當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程能夠成功取得 monitor,並將計數器值設置爲 1,取鎖成功。若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。
複製代碼
synchronized 方法則會被翻譯成普通的方法調用和返回指令如:
invokevirtual、areturn 指令,在 JVM 字節碼層面並無任何特別的指令來實現被synchronized 修飾的方法,
而是在 Class 文件的方法表中將該方法的 access_flags 字段中的 synchronized 標誌位置設置爲 1,
表示該方法是同步方法,並使用調用該方法的對象或該方法所屬的 Class
在 JVM 的內部對象表示 Klass 做爲鎖對象
複製代碼
//省略不必的字節碼 //==================syncTask方法====================== public synchronized void syncTask(); descriptor: ()V //方法標識ACC_PUBLIC表明public修飾,ACC_SYNCHRONIZED指明該方法爲同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 12: 0 line 13: 10 } SourceFile: "SyncMethod.java" 複製代碼
如下部分參考: JVM源碼分析之synchronized實現
一、獲取對象頭的Mark Word;
二、判斷mark是否爲可偏向狀態,即mark的偏向鎖標誌位爲 1,鎖標誌位爲 01;
三、判斷mark中JavaThread的狀態:若是爲空,則進入步驟(4);若是指向當前線程,
則執行同步代碼塊;若是指向其它線程,進入步驟(5);
四、經過CAS原子指令設置mark中JavaThread爲當前線程ID,
若是執行CAS成功,則執行同步代碼塊,不然進入步驟(5);
五、若是執行CAS失敗,表示當前存在多個線程競爭鎖,當達到全局安全點(safepoint),
得到偏向鎖的線程被掛起,撤銷偏向鎖,並升級爲輕量級,升級完成後被阻塞在安全點的線程繼續執行同步代碼塊;
複製代碼
在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。
注意 JVM 提供了關閉偏向鎖的機制, JVM 啓動命令指定以下參數便可
-XX:-UseBiasedLocking
複製代碼
偏向鎖的撤銷:
偏向鎖的 撤銷(revoke) 是一個很特殊的操做, 爲了執行撤銷操做, 須要等待全局安全點(Safe Point),
此時間點全部的工做線程都中止了字節碼的執行。
偏向鎖這個機制很特殊, 別的鎖在執行完同步代碼塊後, 都會有釋放鎖的操做, 而偏向鎖並無直觀意義上的「釋放鎖」操做。
引入一個概念 epoch, 其本質是一個時間戳 , 表明了偏向鎖的有效性
複製代碼
在多線程交替執行同步塊的狀況下,儘可能避免重量級鎖引發的性能消耗,可是若是多個線程在同一時刻進入臨界區,會致使輕量級鎖膨脹升級重量級鎖,因此輕量級鎖的出現並不是是要替代重量級鎖。
一、獲取對象的markOop數據mark;
二、判斷mark是否爲無鎖狀態:mark的偏向鎖標誌位爲 0,鎖標誌位爲 01;
三、若是mark處於無鎖狀態,則進入步驟(4),不然執行步驟(6);
四、把mark保存到BasicLock對象的_displaced_header字段;
五、經過CAS嘗試將Mark Word更新爲指向BasicLock對象的指針,若是更新成功,表示競爭到鎖,則執行同步代碼,不然執行步驟(6);
六、若是當前mark處於加鎖狀態,且mark中的ptr指針指向當前線程的棧幀,則執行同步代碼,不然說明有多個線程競爭輕量級鎖,輕量級鎖須要膨脹升級爲重量級鎖;
複製代碼
重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
鎖膨脹過程:
一、整個膨脹過程在自旋下完成;
二、mark->has_monitor()方法判斷當前是否爲重量級鎖,即Mark Word的鎖標識位爲 10,若是當前狀態爲重量級鎖,執行步驟(3),不然執行步驟(4);
三、mark->monitor()方法獲取指向ObjectMonitor的指針,並返回,說明膨脹過程已經完成;
四、若是當前鎖處於膨脹中,說明該鎖正在被其它線程執行膨脹操做,則當前線程就進行自旋等待鎖膨脹完成,這裏須要注意一點,
雖然是自旋操做,但不會一直佔用cpu資源,每隔一段時間會經過os::NakedYield方法放棄cpu資源,或經過park方法掛起;
若是其餘線程完成鎖的膨脹操做,則退出自旋並返回;
五、若是當前是輕量級鎖狀態,即鎖標識位爲 00
複製代碼
Monitor 競爭:
一、經過CAS嘗試把monitor的_owner字段設置爲當前線程;
二、若是設置以前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
三、若是以前的_owner指向的地址在當前線程中,這種描述有點拗口,換一種說法:以前_owner指向的BasicLock在當前線程棧上,
說明當前線程是第一次進入該monitor,設置_recursions爲1,_owner爲當前線程,該線程成功得到鎖並返回;
四、若是獲取鎖失敗,則等待鎖的釋放;
複製代碼
其本質就是經過CAS設置monitor的_owner字段爲當前線程,若是CAS成功,則表示該線程獲取了鎖,跳出自旋操做,執行同步代碼,不然繼續被掛起;
Monitor 釋放:
當某個持有鎖的線程執行完同步代碼塊時,會進行鎖的釋放,給其它線程機會執行同步代碼,在HotSpot中,經過退出monitor的方式實現鎖的釋放,並通知被阻塞的線程.
鎖消除:
消除鎖是虛擬機另一種鎖的優化,這種優化更完全,
Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),
經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,
經過這種方式消除沒有必要的鎖,能夠節省毫無心義的請求鎖時間
複製代碼
鎖粗化:
將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖。
複製代碼
自旋鎖:
線程的阻塞和喚醒,須要 CPU 從用戶態轉爲核心態。頻繁的阻塞和喚醒對 CPU 來講是一件負擔很重的工做,勢必會給系統的併發性能帶來很大的壓力。
同時,咱們發如今許多應用上面,對象鎖的鎖狀態只會持續很短一段時間。爲了這一段很短的時間,頻繁地阻塞和喚醒線程是很是不值得的
適應性自旋鎖:
自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
複製代碼
鎖升級:
//加鎖 void lock(); //解鎖 void unlock(); //可中斷獲取鎖,與lock()不一樣之處在於可響應中斷操做,即在獲 //取鎖的過程當中可中斷,注意synchronized在獲取鎖時是不可中斷的 void lockInterruptibly() throws InterruptedException; //嘗試非阻塞獲取鎖,調用該方法後當即返回結果,若是可以獲取則返回true,不然返回false boolean tryLock(); //根據傳入的時間段獲取鎖,在指定時間內沒有獲取鎖則返回false,若是在指定時間內當前線程未被中並斷獲取到鎖則返回true boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //獲取等待通知組件,該組件與當前鎖綁定,當前線程只有得到了鎖 //才能調用該組件的wait()方法,而調用後,當前線程將釋放鎖。 Condition newCondition(); 複製代碼
在Java 1.5中,官方在concurrent併發包(J.U.C)
中加入了Lock接口,該接口中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操做進行支持.
Lock 鎖提供的優點:
可使鎖更公平。
可使線程在等待鎖的時候響應中斷。
可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間。
能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖。
複製代碼
AQS 即隊列同步器。它是構建鎖或者其餘同步組件的基礎框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 併發包的做者(Doug Lea)指望它可以成爲實現大部分同步需求的基礎。
數據結構:
//同步隊列頭節點
private transient volatile Node head;
//同步隊列尾節點
private transient volatile Node tail;
//同步狀態
private volatile int state;
複製代碼
AQS 使用一個 int 類型的成員變量 state 來表示同步狀態:
state > 0
時,表示已經獲取了鎖。state = 0
時,表示釋放了鎖。Node構成FIFO的同步隊列來完成線程獲取鎖的排隊工做
參考: 深刻剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理
Sync
:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操做(tryRelease()方法),並提供了lock抽象方法,由其子類實現。
NonfairSync
:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。
FairSync
:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。
AQS、Sync 和 ReentrantLock 的具體關係圖:
構造函數:
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 複製代碼
ReentrantLock 提供兩種實現方式,公平鎖/非公平鎖. 經過構造函數進行初始化 sync
進行判斷當前鎖得類型.
final void lock() { //cas 獲取鎖 if (compareAndSetState(0, 1)) //若是成功設置當前線程Id setExclusiveOwnerThread(Thread.currentThread()); else //不然再次請求同步狀態 acquire(1); } 複製代碼
先對同步狀態執行CAS操做,嘗試把state的狀態從0設置爲1, 若是返回true則表明獲取同步狀態成功,也就是當前線程獲取鎖成,可操做臨界資源,若是返回false,則表示已有線程持有該同步狀態(其值爲1) 獲取鎖失敗,注意這裏存在併發的情景,也就是可能同時存在多個線程設置state變量,所以是CAS操做保證了state變量操做的原子性。返回false後,執行acquire(1)
方法
#acquire(int arg)
方法,爲 AQS 提供的模板方法。該方法爲獨佔式獲取同步狀態,可是該方法對中斷不敏感。也就是說,因爲線程獲取同步狀態失敗而加入到 CLH 同步隊列中,後續對該線程進行中斷操做時,線程不會從 CLH 同步隊列中移除。
acquire
代碼:
public final void acquire(int arg) { //嘗試獲取同步狀態 if (!tryAcquire(arg) && //自旋直到得到同步狀態成功,添加節點到隊列 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 複製代碼
一、tryAcquire
嘗試獲取同步狀態
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //鎖閒置 if (c == 0) { //CAS佔用 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //若是鎖state=1 && 線程爲當前線程 重入鎖的邏輯 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } 複製代碼
二、acquireQueued
加入隊列中,自旋獲取鎖
private Node addWaiter(Node mode) { //將請求同步狀態失敗的線程封裝成結點 Node node = new Node(Thread.currentThread(), mode); Node pred = tail; //若是是第一個結點加入確定爲空,跳過。 //若是非第一個結點則直接執行CAS入隊操做,嘗試在尾部快速添加 if (pred != null) { node.prev = pred; //使用CAS執行尾部結點替換,嘗試在尾部快速添加 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //若是第一次加入或者CAS操做沒有成功執行enq入隊操做 enq(node); return node; } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //獲取前驅節點 final Node p = node.predecessor(); //若是前驅節點試頭節點, 嘗試獲取同步狀態 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 獲取失敗,線程等待 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } 複製代碼
流程圖:
與非公平鎖不一樣的是,在獲取鎖的時,公平鎖的獲取順序是徹底遵循時間上的FIFO規則,也就是說先請求的線程必定會先獲取鎖,後來的線程確定須要排隊,這點與前面咱們分析非公平鎖的nonfairTryAcquire(int acquires)方法實現有鎖不一樣,下面是公平鎖中tryAcquire()方法的實現
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //判斷隊列中是否又線程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //重入鎖邏輯 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } 複製代碼
//ReentrantLock類的unlock public void unlock() { sync.release(1); } //AQS類的release()方法 public final boolean release(int arg) { //嘗試釋放鎖 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //喚醒後繼結點的線程 unparkSuccessor(h); return true; } return false; } //ReentrantLock類中的內部類Sync實現的tryRelease(int releases) protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //判斷狀態是否爲0,若是是則說明已釋放同步狀態 if (c == 0) { free = true; //設置Owner爲null setExclusiveOwnerThread(null); } //設置更新同步狀態 setState(c); return free; } 複製代碼
構造函數:
Lock readLock(); Lock writeLock(); /** 使用默認(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock() { this(false); } /** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } 複製代碼
java.util.concurrent.locks.ReentrantReadWriteLock
,實現 ReadWriteLock
接口,可重入的讀寫鎖實現類。在它內部,維護了一對相關的鎖,一個用於只讀操做,另外一個用於寫入操做。只要沒有 Writer
線程,讀取鎖能夠由多個 Reader
線程同時保持。也就說說,寫鎖是獨佔的,讀鎖是共享的。
在 ReentrantLock 中,使用 Sync ( 實際是 AQS )的 int 類型的 state 來表示同步狀態,表示鎖被一個線程重複獲取的次數。可是,讀寫鎖 ReentrantReadWriteLock 內部維護着一對讀寫鎖,若是要用一個變量維護多種狀態,須要採用「按位切割使用」的方式來維護這個變量,將其切分爲兩部分:高16爲表示讀,低16爲表示寫。
分割以後,讀寫鎖是如何迅速肯定讀鎖和寫鎖的狀態呢?經過位運算。假如當前同步狀態爲S,那麼:
一、readLock
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } protected final int tryAcquireShared(int unused) { //當前線程 Thread current = Thread.currentThread(); int c = getState(); //exclusiveCount(c)計算寫鎖 //若是存在寫鎖,且鎖的持有者不是當前線程,直接返回-1 //存在鎖降級問題,後續闡述 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //讀鎖 int r = sharedCount(c); /* * readerShouldBlock():讀鎖是否須要等待(公平鎖原則) * r < MAX_COUNT:持有線程小於最大數(65535) * compareAndSetState(c, c + SHARED_UNIT):設置讀取鎖狀態 */ if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的狀態,因此要加上2^16 /* * holdCount部分後面講解 */ if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); } 複製代碼
相同點
都實現了多線程同步和內存可見性語義。
都是可重入鎖。
複製代碼
不一樣點
同步實現機制不一樣
synchronized 經過 Java 對象頭鎖標記和 Monitor 對象實現同步。
ReentrantLock 經過CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用於阻塞和解除阻塞)實現同步。
可見性實現機制不一樣
synchronized 依賴 JVM 內存模型保證包含共享變量的多線程內存可見性。
ReentrantLock 經過 ASQ 的 volatile state 保證包含共享變量的多線程內存可見性。
使用方式不一樣
synchronized 能夠修飾實例方法(鎖住實例對象)、靜態方法(鎖住類對象)、代碼塊(顯示指定鎖對象)。
ReentrantLock 顯示調用 tryLock 和 lock 方法,須要在 finally 塊中釋放鎖。
功能豐富程度不一樣
synchronized 不可設置等待時間、不可被中斷(interrupted)。
ReentrantLock 提供有限時間等候鎖(設置過時時間)、可中斷鎖(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等豐富功能
鎖類型不一樣
synchronized 只支持非公平鎖。
ReentrantLock 提供公平鎖和非公平鎖實現。固然,在大部分狀況下,非公平鎖是高效的選擇。
複製代碼