淺談Java并發(fā)之同步器設(shè)計(jì)
前言:
在 Java并發(fā)內(nèi)存模型詳情了解到多進(jìn)程(線程)讀取共享資源的時(shí)候存在競(jìng)爭(zhēng)條件。
計(jì)算機(jī)中通過(guò)設(shè)計(jì)同步器來(lái)協(xié)調(diào)進(jìn)程(線程)之間執(zhí)行順序。同步器
作用就像登機(jī)安檢人員一樣可以協(xié)調(diào)旅客按順序通過(guò)。
在Java
中,同步器
可以理解為一個(gè)對(duì)象,它根據(jù)自身狀態(tài)協(xié)調(diào)線程的執(zhí)行順序。比如鎖(Lock
),信號(hào)量(Semaphore
),屏障(CyclicBarrier
),阻塞隊(duì)列(Blocking Queue
)。
這些同步器在功能設(shè)計(jì)上有所不同,但是內(nèi)部實(shí)現(xiàn)上有共通的地方。
1、同步器
同步器的設(shè)計(jì)一般包含幾個(gè)方面:狀態(tài)變量設(shè)計(jì)(同步器內(nèi)部狀態(tài)),訪問(wèn)條件設(shè)定,狀態(tài)更新,等待方式,通知策略。
訪問(wèn)條件是控制線程是否能執(zhí)行(訪問(wèn)共享對(duì)象)的條件,它往往與狀態(tài)變量緊密相關(guān)。而通知策略是線程釋放鎖定狀態(tài)后通知其它等待線程的方式,一般有以下幾種情況:
- 通知所有等待的線程。
- 通知1個(gè)隨機(jī)的N個(gè)等待線程。
- 通知1個(gè)特定的N個(gè)等待線程
看下面例子,通過(guò)鎖方式的同步器
public class Lock{ // 狀態(tài)變量 isLocked private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ // 訪問(wèn)條件 當(dāng)isLocked=false 時(shí)獲得訪問(wèn)權(quán)限否則等待 while(isLocked){ // 阻塞等待 wait(); } //狀態(tài)更新 線程獲得訪問(wèn)權(quán)限 isLocked = true; } public synchronized void unlock(){ //狀態(tài)更新 線程釋放訪問(wèn)權(quán)限 isLocked = false; // 通知策略 object.notify | object.notifyAll notify(); } }
我們用計(jì)數(shù)信號(hào)量控制同時(shí)執(zhí)行操作活動(dòng)數(shù)。這里模擬一個(gè)連接池。
public class PoolSemaphore { // 狀態(tài)變量 actives 計(jì)數(shù)器 private int actives = 0; private int max; public PoolSemaphore(int max) { this.max = max; } public synchronized void acquire() throws InterruptedException { //訪問(wèn)條件 激活數(shù)小于最大限制時(shí),獲得訪問(wèn)權(quán)限否則等待 while (this.actives == max) wait(); //狀態(tài)更新 線程獲得訪問(wèn)權(quán)限 this.actives++; // 通知策略 object.notify | object.notifyAll this.notify(); } public synchronized void release() throws InterruptedException { //訪問(wèn)條件 激活數(shù)不為0時(shí),獲得訪問(wèn)權(quán)限否則等待 while (this.actives == 0) wait(); //狀態(tài)更新 線程獲得訪問(wèn)權(quán)限 this.actives--; // 通知策略 object.notify | object.notifyAll this.notify(); } }
1.1 原子指令
同步器設(shè)計(jì)里面,最重要的操作邏輯是“如果滿足條件,以更新狀態(tài)變量來(lái)標(biāo)志線程獲得或釋放訪問(wèn)權(quán)限”,該操作應(yīng)具備原子性。
比如test-and-set
計(jì)算機(jī)原子指令,意思是進(jìn)行條件判斷滿足則設(shè)置新值。
function Lock(boolean *lock) { while (test_and_set(lock) == 1); }
另外還有很多原子指令 fetch-and-add compare-and-swap
,注意這些指令需硬件支持才有效。
同步操作中,利用計(jì)算機(jī)原子指令,可以避開鎖,提升效率。java
中沒(méi)有 test-and-set
的支持,不過(guò) java.util.concurrent.atomic
給我們提供了很多原子類API,里面支持了 getAndSet
和compareAndSet
操作。
看下面例子,主要在區(qū)別是等待方式不一樣,上面是通過(guò)wait()
阻塞等待,下面是無(wú)阻塞循環(huán)。
public class Lock{ // 狀態(tài)變量 isLocked private AtomicBoolean isLocked = new AtomicBoolean(false); public void lock() throws InterruptedException{ // 等待方式 變?yōu)樽孕却? while(!isLocked.compareAndSet(false, true)); //狀態(tài)更新 線程獲得訪問(wèn)權(quán)限 isLocked.set(true); } public synchronized void unlock(){ //狀態(tài)更新 線程釋放訪問(wèn)權(quán)限 isLocked.set(false); } }
1.2 關(guān)于阻塞擴(kuò)展說(shuō)明
阻塞意味著需要將進(jìn)程或線程狀態(tài)進(jìn)行轉(zhuǎn)存,以便還原后恢復(fù)執(zhí)行。這種操作是昂貴繁重,而線程基于進(jìn)程之上相對(duì)比較輕量。線程的阻塞在不同編程平臺(tái)實(shí)現(xiàn)方式也有所不同,像Java
是基于JVM
運(yùn)行,所以它由JVM
完成實(shí)現(xiàn)。
在《Java Concurrency in Practice》中,作者提到
競(jìng)爭(zhēng)性同步可能需要OS活動(dòng),這增加了成本。當(dāng)爭(zhēng)用鎖時(shí),未獲取鎖的線程必須阻塞。 JVM可以通過(guò)旋轉(zhuǎn)等待(反復(fù)嘗試獲取鎖直到成功)來(lái)實(shí)現(xiàn)阻塞,也可以通過(guò)操作系統(tǒng)掛起阻塞的線程來(lái)實(shí)現(xiàn)阻塞。哪種效率更高取決于上下文切換開銷與鎖定可用之前的時(shí)間之間的關(guān)系。對(duì)于短暫的等待,最好使用自旋等待;對(duì)于長(zhǎng)時(shí)間的等待,最好使用暫停。一些JVM基于對(duì)過(guò)去等待時(shí)間的分析數(shù)據(jù)來(lái)自適應(yīng)地在這兩者之間進(jìn)行選擇,但是大多數(shù)JVM只是掛起線程等待鎖定。
從上面可以看出JVM實(shí)現(xiàn)阻塞兩種方式
- 旋轉(zhuǎn)等待(
spin-waiting
),簡(jiǎn)單理解是不暫停執(zhí)行,以循環(huán)的方式等待,適合短時(shí)間場(chǎng)景。 - 通過(guò)操作系統(tǒng)掛起線程。
JVM中通過(guò) -XX: +UseSpinning
開啟旋轉(zhuǎn)等待, -XX: PreBlockSpi =10
指定最大旋轉(zhuǎn)次數(shù)。
2、AQS
AQS
是AbstractQueuedSynchronizer
簡(jiǎn)稱。本節(jié)對(duì)AQS
只做簡(jiǎn)單闡述,并不全面。
java.util.concurrent
包中的 ReentrantLock
,CountDownLatch
,Semaphore
,CyclicBarrier
等都是基于是AQS
同步器實(shí)現(xiàn)。
狀態(tài)變量 是用 int state
來(lái)表示,狀態(tài)的獲取與更新通過(guò)以下API操作。
int getState() void setState(int newState) boolean compareAndSetState(int expect, int update)
該狀態(tài)值在不同API中有不同表示意義。比如ReentrantLock
中表示持有鎖的線程獲取鎖的次數(shù),Semaphore
表示剩余許可數(shù)。
關(guān)于等待方式和通知策略
的設(shè)計(jì)
AQS
通過(guò)維護(hù)一個(gè)FIFO
同步隊(duì)列(Sync queue
)來(lái)進(jìn)行同步管理。當(dāng)多線程爭(zhēng)用共享資源時(shí)被阻塞入隊(duì)。而線程阻塞與喚醒是通過(guò) LockSupport.park/unpark API
實(shí)現(xiàn)。
它定義了兩種資源共享方式。
Exclusive
(獨(dú)占,只有一個(gè)線程能執(zhí)行,如ReentrantLock
)Share
(共享,多個(gè)線程可同時(shí)執(zhí)行,如Semaphore/CountDownLatch
)
每個(gè)節(jié)點(diǎn)包含waitStatus
(節(jié)點(diǎn)狀態(tài)),prev
(前繼),next
(后繼),thread
(入隊(duì)時(shí)線程),nextWaiter
(condition
隊(duì)列的后繼節(jié)點(diǎn))
waitStatus
有以下取值:
CANCELLED(1)
表示線程已取消。當(dāng)發(fā)生超時(shí)或中斷,節(jié)點(diǎn)狀態(tài)變?yōu)槿∠?,之后狀態(tài)不再改變。SIGNAL(-1)
表示后繼節(jié)點(diǎn)等待前繼的喚醒。后繼節(jié)點(diǎn)入隊(duì)時(shí),會(huì)將前繼狀態(tài)更新為SIGNAL。CONDITION(-2)
表示線程在Condition queue
里面等待。當(dāng)其他線程調(diào)用了Condition.signal()
方法后,CONDITION
狀態(tài)的節(jié)點(diǎn)將從Condition queue
轉(zhuǎn)移到Sync queue
,等待獲取鎖。PROPAGATE(-3)
在共享模式下,當(dāng)前節(jié)點(diǎn)釋放后,確保有效通知后繼節(jié)點(diǎn)。- (0) 節(jié)點(diǎn)加入隊(duì)列時(shí)的默認(rèn)狀態(tài)。
AQS 幾個(gè)關(guān)鍵 API
tryAcquire(int)
獨(dú)占方式下,嘗試去獲取資源。成功返回true
,否則false
。tryRelease(int)
獨(dú)占方式下,嘗試釋放資源,成功返回true
,否則false
。tryAcquireShared(int)
共享方式下,嘗試獲取資源。返回負(fù)數(shù)為失敗,零和正數(shù)為成功并表示剩余資源。tryReleaseShared(int)
共享方式下,嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待節(jié)點(diǎn)返回true
,否則false
。isHeldExclusively()
判斷線程是否正在獨(dú)占資源。
2.1 acquire(int arg)
public final void acquire(int arg) { if ( // 嘗試直接去獲取資源,如果成功則直接返回 !tryAcquire(arg) && //線程阻塞在同步隊(duì)列等待獲取資源。等待過(guò)程中被中斷,則返回true,否則false acquireQueued( // 標(biāo)記該線程為獨(dú)占方式,并加入同步隊(duì)列尾部。 addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
2.2 release(int arg)
public final boolean release(int arg) { // 嘗試釋放資源 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 喚醒下一個(gè)線程(后繼節(jié)點(diǎn)) unparkSuccessor(h); return true; } return false; } private void unparkSuccessor(Node node) { .... Node s = node.next; // 找到后繼節(jié)點(diǎn) if (s == null || s.waitStatus > 0) {//無(wú)后繼或節(jié)點(diǎn)已取消 s = null; // 找到有效的等待節(jié)點(diǎn) for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); // 喚醒線程 }
總結(jié):
文章記錄并發(fā)編程中同步器設(shè)計(jì)的一些共性特征。并簡(jiǎn)單介紹了Java中的AQS。
到此這篇關(guān)于淺談Java并發(fā)之同步器設(shè)計(jì)的文章就介紹到這了,更多相關(guān)Java并發(fā)之同步器設(shè)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java并發(fā)內(nèi)存模型詳情
- Java并發(fā)編程之阻塞隊(duì)列深入詳解
- Java 淺談 高并發(fā) 處理方案詳解
- Java并發(fā)編程之線程中斷
- Java并發(fā)編程之threadLocal
- Java實(shí)現(xiàn)計(jì)算器設(shè)計(jì)
- Java中轉(zhuǎn)換器設(shè)計(jì)模式深入講解
- Java編程Iterator迭代器設(shè)計(jì)原理及實(shí)現(xiàn)代碼示例
- Java裝飾器設(shè)計(jì)模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
- Java裝飾器設(shè)計(jì)模式初探
相關(guān)文章
spring-data-jpa使用自定義repository來(lái)實(shí)現(xiàn)原生sql
這篇文章主要介紹了在spring-data-jpa中使用自定義repository來(lái)實(shí)現(xiàn)原生sql,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11springboot下實(shí)現(xiàn)RedisTemplate?List?清空
我們經(jīng)常會(huì)使用Redis的List數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)一系列的元素,當(dāng)我們需要清空一個(gè)List時(shí),可以使用RedisTemplate來(lái)實(shí)現(xiàn),本文就來(lái)詳細(xì)的介紹一下如何實(shí)現(xiàn),感興趣的可以了解一下2024-01-01Java編譯錯(cuò)誤問(wèn)題:需要class,interface或enum
這篇文章主要介紹了Java編譯錯(cuò)誤問(wèn)題:需要class,interface或enum,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02用Java實(shí)現(xiàn)春聯(lián)?支持自定義字體顏色
大家好,本篇文章主要講的是用Java編寫春聯(lián)?支持自定義字體顏色,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-01-01SpringMVC?RESTFul實(shí)戰(zhàn)案例刪除功能實(shí)現(xiàn)
這篇文章主要為大家介紹了SpringMVC?RESTFul實(shí)戰(zhàn)案例刪除功能實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Java Annotation(Java 注解)的實(shí)現(xiàn)代碼
本篇文章介紹了,Java Annotation(Java 注解)的實(shí)現(xiàn)代碼。需要的朋友參考下2013-05-05