多线程—Lock、Condition、ReentrantLock、ReentrantReadWriteLock
Lock接口
public interface Lock { //下面4個方法都是獲得鎖void lock(); void lockInterruptibly() throws InterruptedException; // 如果當前線程未被中斷,則獲取鎖,可以響應中斷 boolean tryLock(); //如果獲取到鎖返回true,否則falseboolean tryLock(long var1, TimeUnit var3) throws InterruptedException; //如果獲取不到鎖,就等待一段時間,超時返回false。 //解除鎖,在finally里調(diào)用void unlock(); //返回Condition實例Condition newCondition(); }對于Lock接口方法的實現(xiàn),大多都是調(diào)用AQS的方法來實現(xiàn)。實現(xiàn)Lock接口的類含有繼承了AQS一個內(nèi)部類(例如ReentrantLock的Sync內(nèi)部類),從而調(diào)用內(nèi)部類的繼承自AQS的方法或者重寫的方法來實現(xiàn)Lock接口的方法。
Lock接口最后還有一個返回Condition實例的方法。Condition是和Lock配合使用的。
在一個AQS同步器中,可以定義多個Condition,每一個Condition是一條FIFO隊列。只需要多次lock.newCondition(),每次都會返回一個新的ConditionObject對象。在ConditionObject中,通過一個條件隊列來維護等待的線程,這個隊列跟AQS的隊列不是同一條隊列,一個同步器中可以有多個條件隊列。
Condition接口
public interface Condition {// 當前線程進入等待狀態(tài)直到被通知(signal)或被中斷void await() throws InterruptedException;// 不響應中斷等待,直到被通知(signal)void awaitUninterruptibly();// 等待指定時長直到被通知或中斷或超時。long awaitNanos(long nanosTimeout) throws InterruptedException;// 等待指定時長直到被通知或中斷或超時。boolean await(long time, TimeUnit unit) throws InterruptedException;// 當前線程進入等待狀態(tài)直到被通知、中斷或者到某個時間。如果沒有到指定事件就被通知,方法返回true,否則false。 boolean awaitUntil(Date deadline) throws InterruptedException;// 喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關(guān)聯(lián)的鎖 void signal();// 喚醒所有等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關(guān)聯(lián)的鎖 void signalAll(); }ConditionObject
ConditionObject是AQS中的內(nèi)部類,提供了條件鎖的同步實現(xiàn),實現(xiàn)了Condition接口,并且實現(xiàn)了其中的await(),signal(),signalALL()等方法。因為Condition的操作需要獲取相關(guān)聯(lián)的鎖,所以作為同步器的內(nèi)部類也較為合理。每個Condition對象都包含著一個FIFO等待隊列,該隊列是Condition對象實現(xiàn)等待/通知功能的關(guān)鍵。
當線程獲取到鎖之后,Condition對象調(diào)用await相關(guān)的方法:
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 添加當前線程到條件隊列Node node = addConditionWaiter();// 釋放已經(jīng)獲取的鎖資源,并返回釋放前的同步狀態(tài)int savedState = fullyRelease(node);int interruptMode = 0;// 如果當前節(jié)點不在同步隊列中,線程進入阻塞狀態(tài),等待被喚醒while (!isOnSyncQueue(node)) {LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode); }可以看到因為ConditionObject是AQS內(nèi)部類,可以獲取到外部類的數(shù)據(jù),調(diào)用await將線程放進條件隊列,然后同樣調(diào)用LockSupport類的park方法進行阻塞。
Condition對象調(diào)用signal或者signalAll方法時:
// 將條件隊列中第一個有效的元素移除并且添加到同步隊列中 private void doSignal(Node first) {do {if ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;first.nextWaiter = null;// 將條件隊列中等待最久的那個有效元素添加到同步隊列中} while (!transferForSignal(first) &&(first = firstWaiter) != null); }// 將條件隊列中的節(jié)點轉(zhuǎn)換到同步隊列中 final boolean transferForSignal(Node node) {// 如果節(jié)點的等待狀態(tài)不能被修改,說明當前線程已經(jīng)被取消等待if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 加入到同步隊列中,并且嘗試將前驅(qū)節(jié)點設(shè)置為可喚醒狀態(tài)Node p = enq(node); int ws = p.waitStatus;// 如果前驅(qū)節(jié)點不需要喚醒,或者設(shè)置狀態(tài)為‘喚醒’失敗,則喚醒線程時期重新爭奪同步狀態(tài)if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);return true; }singal方法將節(jié)點放回同步隊列尾端,調(diào)用enq方法,該方法在AQS的時候分析過,該方法使用了死循環(huán), 即以自旋方式將節(jié)點插入隊列,如果失敗則不停的嘗試, 直到成功為止,運用到了樂觀鎖的原理,該方法也負責在隊列為空時, 初始化隊列。
為什么要有Condition呢?
在synchronized中,我們可以調(diào)用object.await和object.notify,讓線程等待和喚醒,但方法僅在synchronized中可以使用。通過Condition能夠更加精細的控制多線程的休眠與喚醒,synchronized相比ReentrantLock、ReadWriteLock等實現(xiàn)了Lock接口和內(nèi)含繼承AQS類的子類的鎖來說,synchronized顯得笨重,不夠靈活,適合小段代碼使用。
condition.signal和object.notify區(qū)別
- obj.notify();? ?隨機喚醒一個處于等待狀態(tài)的線程,可能有多個線程處于等待狀態(tài),繼續(xù)執(zhí)行wait后面的代碼。與synchronized同步關(guān)鍵字配合
- condition.signal(); 喚醒在同條件隊列的線程,與Lock配合
使用要求:
- signal()、await()、signalAll()方法使用之前必須要先進行l(wèi)ock()獲取鎖,類似使用Object的notify()、wait()、notifyAll()之前必須要對Object對象進行synchronized操作,且兩類方法不能混合使用,否則都會拋IIlegalMonitorStateException。
Synchronized和Lock比較
- Synchronized是關(guān)鍵字,內(nèi)置語言實現(xiàn),Lock是接口。
- Synchronized在線程發(fā)生異常時會自動釋放鎖,因此不會發(fā)生異常死鎖。Lock異常時不會自動釋放鎖,所以需要在finally中實現(xiàn)釋放鎖。
- Lock跟Synchronized可以在線程sleep,await,wait的時候響應中斷,但是Lock還可以在等待獲取鎖的時候響應中斷,對應的是lockInterruptibly()方法,而Synchronized只能一直等著。
- Lock可以使用讀鎖提高多線程讀效率。
關(guān)系圖:
重入鎖
可重入就是說某個線程已經(jīng)獲得某個鎖,可以再次獲取鎖而不會出現(xiàn)死鎖。
為什么需要再次獲取鎖呢?
一個線程在執(zhí)行一個帶鎖的方法,該方法中又調(diào)用了另一個需要相同鎖的方法,則該線程可以直接執(zhí)行調(diào)用的方法,而無需重新獲得鎖,從而避免了死鎖。
實現(xiàn)原理
通過為每個鎖關(guān)聯(lián)一個請求計數(shù)器和一個獲得該鎖的線程。該計數(shù)器不是AQS的state,當計數(shù)器為0時,認為鎖是未被占用的。線程請求一個未被占用的鎖時,JVM將記錄該線程并將請求計數(shù)器設(shè)置為1,此時該線程就獲得了鎖,當該線程再次請求這個鎖,計數(shù)器將遞增,當線程退出同步方法或者同步代碼塊時,計數(shù)器將遞減,當計數(shù)器為0時,線程就釋放了該對象,其他線程才能獲取該鎖。
重入鎖有:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
兩者區(qū)別
- synchronized是獨占鎖,加鎖和解鎖的過程自動進行,易于操作,但不夠靈活。ReentrantLock也是獨占鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。
- synchronized可重入,因為加鎖和解鎖自動進行,不必擔心最后是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖需要手動進行,且次數(shù)需一樣,否則其他線程無法獲得鎖。
- synchronized不可響應中斷,一個線程獲取不到鎖就一直等著;ReentrantLock可以相應中斷。
ReentrantLock
ReentrantLock的內(nèi)部類Sync繼承了AQS,分為公平鎖FairSync和非公平鎖NonfairSync。
Lock lock=new ReentrantLock();//默認非公平鎖 Lock lock=new ReentrantLock(true);//公平鎖 Lock lock=new ReentrantLock(false);//非公平鎖 getHoldCount() 查詢當前線程保持此鎖的次數(shù),也就是執(zhí)行此線程執(zhí)行l(wèi)ock方法的次數(shù)。getQueueLength()返回正等待獲取此鎖的線程數(shù),比如啟動10個線程,1個線程獲得鎖,此時返回的是9。getWaitQueueLength(Condition condition)返回在該條件隊列的線程數(shù)。hasWaiters(Condition condition) 查詢該條件隊列是否有等待線程。hasQueuedThread(Thread thread) 查詢指定的線程是否正在等待獲取Lock鎖。hasQueuedThreads() 是否有線程等待此鎖isFair() 該鎖是否公平鎖isHeldByCurrentThread() 當前線程是否保持鎖鎖定,線程的執(zhí)行l(wèi)ock方法的前后分別是false和trueisLock() 此鎖是否有任意線程占用lockInterruptibly() 如果當前線程未被中斷,獲取鎖tryLock() 線程嘗試獲取鎖,如果獲取成功,返回 true,否則返回 falsetryLock(long timeout,TimeUnit unit) 線程如果在指定等待時間內(nèi)獲得了鎖,就返回true,否則返回 falseReentrantReadWriteLock
Java并發(fā)包中ReadWriteLock是一個接口,主要有兩個方法,如下:
public interface ReadWriteLock {//返回讀鎖Lock readLock();//返回寫鎖Lock writeLock(); }ReetrantReadWriteLock實現(xiàn)了ReadWriteLock接口并添加了可重入的特性。
有兩個鎖,一個是讀操作相關(guān)的鎖,稱為共享鎖;一個是寫相關(guān)的鎖,稱為排他鎖。
讀寫鎖的機制:
- "讀-讀" 不互斥
- "讀-寫" 互斥
- "寫-寫" 互斥
線程進入讀鎖的前提條件:
- 沒有其他線程的寫鎖或者有寫請求,但調(diào)用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:
- 沒有其他線程的讀鎖
- 沒有其他線程的寫鎖
3個特性:
- 公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平。
- 重進入:讀鎖和寫鎖都支持線程重進入。
- 鎖降級: 重入允許從寫入鎖降級為讀取鎖,其實現(xiàn)方式是:先獲取寫入鎖,然后獲取讀取鎖,最后釋放寫入鎖。但是,從讀取鎖升級到寫入鎖是不可能的。
為什么需要鎖降級:
線程A獲取寫鎖然后操作完變量x(后面我們還要利用變量x來進行一些操作),如果此時沒有獲取讀鎖然后釋放寫鎖,就可能發(fā)生其他線程獲取寫鎖,然后更新了變量x,從而讓我們后面用變量x的操作發(fā)生錯誤。如果有了讀鎖,其他線程就獲取不了寫鎖,而能獲取讀鎖,提高效率也不會發(fā)生錯誤。
?
?
?
?
?
?
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的多线程—Lock、Condition、ReentrantLock、ReentrantReadWriteLock的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你是否发现 职业能力危机,请 警惕
- 下一篇: 多线程—Thread类及线程三种创建方式