常用併發工具原理分析
一、包含的知識點
- Condition原理
- CountDownLatch原理
- CyclicBarrier原理
- Semaphore原理
二、Condition原理
2.1 Condition簡單使用
沒有Lock之前, 線程間通信可以通過wait()/notify()、notifyAll()來進行通信, 在Doug Lea提供了Lock之後, 有沒有新的方式進行線程間通信了呢 ? 答案肯定是有的。 它的實現方式可以參考下面的代碼
//await實現
public class ConditionWait implements Runnable{
private Lock lock ;
private Condition condition ;
public ConditionWait(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("ConditionWait start ...");
try {
lock.lock();
condition.await();
System.out.println("ConditionWait await ...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("ConditionWait end ...");
}
}
//signal實現
public class ConditionSignal implements Runnable {
private Lock lock ;
private Condition condition ;
public ConditionSignal(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("ConditionSignal start ...");
try{
lock.lock();
condition.signal();
System.out.println("ConditionSignal signal ...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("ConditionSignal end ...");
}
}
//main
public class ConditionMain {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition() ;
ConditionWaitDemo waitDemo = new ConditionWaitDemo(lock, condition) ;
ConditionSignalDemo signalDemo = new ConditionSignalDemo(lock, condition) ;
new Thread(waitDemo).start();
new Thread(signalDemo).start();
Thread.sleep(3000);
}
}
Condition調用await方法後, 當前線程會釋放鎖並等待, 當其它線程調用Condition.signal/signalAll方法,會通知被阻塞的線程, 被喚醒的線程在獲得鎖之後會繼續執行。
上面的代碼是通過Condition實現wait/notify功能, 除此之外,也可以通過Condition實現生產者(Producer)/消費者(Consumer)等其它功能, 具體實現邏輯大家可以Google查詢一下。
2.2 Condition await源碼分析
從2.1節ConditionWait代碼可以看到, 線程執行await之前需要獲得鎖, 執行wait操作時, 再調用await方法
lock.lock();
condition.await();
condition.await()
首先我們看下await()的實現邏輯
public final void await() throws InterruptedException {
if (Thread.interrupted()) // 檢測當前線程是否被中斷了
throw new InterruptedException();
Node node = addConditionWaiter(); // 創建一個狀態爲CONDITION的節點
int savedState = fullyRelease(node); //釋放當前線程持有的鎖, 因爲鎖具有重入性, 這裏釋放鎖時會將state清0, savedState保存重入次數記錄
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 如果當前線程沒有在同步隊列上, 就將當前線程掛起
LockSupport.park(this); // 掛起當前線程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/**
* 當線程被喚醒時, 會嘗試拿鎖
* 如果獲取鎖成功, 並且中斷模式 != THROW_IE(-1), 將中斷模式設置爲REINTERRUPT(1)
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果當前節點存在後續節點, 會將後續節點中CANCELLED狀態的給清理掉
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0) // 如果線程被中斷, 需要拋出異常或重新中斷
reportInterruptAfterWait(interruptMode);
}
- 首先會檢測當前線程是否被中斷了, 如果中斷了會拋出異常
- 創建一個狀態爲CONDITION的節點, 並釋放當前線程持有的鎖
- 如果當前線程沒有在同步隊列上, 就將當前線程掛起
- 如果喚醒的線程獲取鎖成功, 並且中斷模式 != THROW_IE(-1), 就將中斷模式設置爲REINTERRUPT(1)
- 如果當前節點存在後續節點, 會將後續節點中CANCELLED狀態的給清理掉
addConditionWaiter()
將當前線程創建爲節點狀態爲CONDITION的新Node, , 並添加到等待隊列中, 這個隊列是單向鏈表, firstWaiter指向隊列首節點, lastWaiter指向隊列的尾節點
/**
* Adds a new waiter to wait queue.
* @return its new wait node
*/
private Node addConditionWaiter() {
Node t = lastWaiter; // 獲取鏈表中尾節點
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) { // 如果尾節點不是CONDITION類型, 清除CANCELLED狀態節點
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 將當前線程創建爲狀態爲CONDITION的新節點
if (t == null) // 鏈表是空鏈表
firstWaiter = node;
else // 鏈表非空, 原尾節點指向當前新節點
t.nextWaiter = node;
lastWaiter = node; // 將當前節點設置爲新的尾節點
return node;
}
fullyRelease()
釋放當前線程鎖, 並清除重入記錄state, 返回當前線程的重入記錄值savedState, 注意和tryRelease()方法的區別
/**
* Invokes release with current state value; returns saved state.
* Cancels node and throws exception on failure.
* @param node the condition node for this wait
* @return previous sync state
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); // 獲取重入次數
if (release(savedState)) { //釋放鎖, 並喚醒同步隊列中下一個線程
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed) // 如果釋放鎖失敗, 將當前設置爲CANCELLED狀態
node.waitStatus = Node.CANCELLED;
}
}
//ReentrantLock原理分析.md 已經分析了這個方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 釋放鎖成功, 同步隊列喚醒後續節點
return true;
}
return false;
}
isOnSyncQueue()
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
從前面的知識點, 我們知道Condition會維護一個自己的單向鏈表, 將創建的等待節點Node存入鏈表中, 如果Condition調用了signal方法,會將等待隊列中的節點加入到同步隊列中, 然後嘗試獲取鎖, isOnSyncQueue方法的作用是校驗節點是否在同步隊列中
- 如果return false , 表示節點不在AQS中, 當前節點在等待隊列中,沒有被喚醒去爭搶鎖, 需要等待其它線程調用signal方法來喚醒(注: node.prev == null 表示當前節點已經獲取了鎖資源
- 如果return true, 意味着節點在AQS隊列中, 需要去競爭鎖資源
具體代碼分析邏輯是
- node.waitStatus == Node.CONDITION, 節點狀態是CONDITION, 在等待隊列中
- node.prev == null, 是head節點, 當前節點已經獲取了鎖
- node.next != null, 說明當前節點的狀態不是CONDITION,存在後繼節點, 則肯定在AQS隊列中
- findNodeFromTail, 從AQS隊列尾部開始查詢節點是否存在於雙端隊列中, 如果發現說明存在AQS中
reportInterruptAfterWait()
根據interruptMode中斷標識, 來進行中斷上報
- THROW_IE,則拋出中斷異常
- REINTERRUPT,則重新響應中斷
acquireQueued()方法在 ReentrantLock原理分析.md 文章中已經講解, 這裏不再分析。
2.3 Condition signal源碼分析
執行Condition.await方法後, 線程會加入等待隊列, 調用Condition.signal方法來激活, 下面是signal方法的入口, 核心方法是doSignal
public final void signal() {
if (!isHeldExclusively()) // 當前線程是否以獨佔方式持有監視器鎖, 如果不是拋出異常
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 等待隊列首節點
if (first != null)
doSignal(first); // 存在首節點, 找到第一個CONDITION節點進行signal操作
}
doSignal()
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null; //first節點已經處理過了, 將nextWaiter設置爲null
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 如果節點類型是CONDITION, 加入到AQS隊列中
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//更新節點狀態爲0, 如果更新失敗說明節點是CANCELLED狀態
return false;
Node p = enq(node); // 將當前節點添加到AQS隊列中, 並且返回原隊列的尾節點
int ws = p.waitStatus;
// 如果節點是CANCELLED狀態 或者CAS設置節點爲SIGNAL失敗, 喚醒當前線程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;//如果node的prev節點是signal狀態, 那麼被阻塞線程的喚醒操作由AQS來完成
}
從Condition隊列首部開始, 查找一個類型是CONDITION的節點, 執行transferForSignal方法, 將節點從Condition等待隊列移入到AQS等待隊列中,同時修改原同步隊列尾節點的狀態。
transferForSignal方法通過CAS方式修改節點狀態, 如果成功從CONDITION狀態修改爲默認狀態(0), 將節點添加到AQS隊列中, 然後喚醒這個節點對應的線程, 線程重新執行。
2.4 被阻塞的線程喚醒後的操作
在2.2節講解await方法時checkInterruptWhileWaiting邏輯沒有具體分析, 那麼它的作用是什麼呢 ?分析邏輯: 如果當前線程不在同步隊列中, 那麼該線程會被掛起, 然後檢測線程在等待過程中有沒有被其它線程觸發過中斷請求 。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
執行Condition.signal方法後, 被喚醒的線程會執行if邏輯, 校驗當前線程的中斷情況, 下面我們來分析方法checkInterruptWhileWaiting
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? // 如果線程被中斷了, 執行transferAfterCancelledWait方法判斷是拋出異常(THROW_IE)還是重新中斷(REINTERRUPT)
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
// 使用CAS修改節點狀態, 如果成功,對node節點進行入隊操作
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/**
* 如果CAS失敗, 判斷當前node節點是否已經在AQS隊列上
* 如果不在, yield操作
* 如果在, 返回false
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
transferAfterCancelledWait在進行CAS操作的時候可能會失敗, 但是它的失敗可能是:
- 發生了中斷操作
- 發生了signal操作
爲了確定具體的操作,需要先將當前節點通過enq方法入隊, 然後返回false給checkInterruptWhileWaiting, 再返回REINTERRUPT(1), 用於後續重新中斷。
checkInterruptWhileWaiting方法代表當前線程是否在park的時候被中斷喚醒,
- 如果爲true, 表示中斷在signal之前, 需要拋出InterruptedException
- 如果爲false, 表示執行了signal操作, 需要重新中斷
2.5 總結
線程 awaitThread 通過lock.lock()方法獲取鎖成功後調用了 condition.await 方法進入等待隊列,而另一個線程signalThread 通過 lock.lock()方法成功獲取鎖後, 執行condition.signal 或者 signalAll 方法,使得awaitThread線程能夠有機會移入到同步隊列,當其他線程釋放 lock 後,使得awaitThread線程能夠有機會獲取 lock,從而使得awaitThread線程能夠從 await 方法中退出,進行後續操作。如果 awaitThread 獲取 lock 失敗會直接進入到同步隊列
對上面流程圖做下面說明
- 阻塞,await方法中, 線程釋放鎖資源後, 如果節點不在AQS同步隊列, 則阻塞當前節點, 如果在同步隊列, 則通過自旋方式嘗試獲取鎖
- 釋放, signal後, 節點會從Condition等待隊列進入AQS同步隊列, 進入正常的鎖獲取流程
三、CountDownLatch
3.1 CountDownLatch作用
CountDownLatch是一個工具類, 調用await方法後, 它允許一個或者多個線程一直等待, 直到其它線程操作執行結束, c中核心方法
-
countdown
每調用一次, state的值就減1, 當state=0時, 會喚醒AQS隊列中的線程
-
await
調用了await方法, 如果state不爲0, 線程將加入AQS隊列進行等待
之前我們有提到AQS.state這個字段,它在不同併發類中有不同的作用, 比如重入鎖中, state表示重入次數;那CountDownLatch中是什麼意思,它是怎麼指定的呢 ?
//1. 創建對象
CountDownLatch downLatch = new CountDownLatch(3);
//2. CountDownLatch構造函數
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//3. Sync構造函數
Sync(int count) {
setState(count);
}
protected final void setState(int newState) {
state = newState;
}
從上面創建CountDownLatch的代碼, 可以看出state就是入參的值, 用作計數器, 通過countdown來進行遞減。下面是相關圖示
3.2 CountDownLatch await源碼分析
在進行源碼講解前, 請先看下類繼承圖和CountDownLatch類結構圖,
針對CountDownLatch類,其核心方法是await、countDown,這裏我們先講解await方法
//1. await方法入口
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//2. 中斷方式獲取共享鎖
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 中斷了就拋出異常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg); // wait核心邏輯, state !- 0 , 當前線程需要加入共享鎖隊列
}
-
首先檢測線程是否被中斷過, 如果被中斷了, 拋出中斷異常
-
嘗試獲取鎖, 即校驗 state 是否等於 0 , true 返回1,false返回-1
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
-
獲取鎖失敗, state !=0 , 將線程加入AQS隊列
doAcquireSharedInterruptibly
注: 在執行鎖匙放時, 如果unparkSuccessor成功, 說明有新的線程佔有了鎖, 代碼會重新回到doAcquireSharedInterruptibly中執行,被喚醒的線程進入下一次循環判斷; state=0時, 會執行setHeadAndPropagate方法
/**
* Acquires in shared interruptible mode.
* 以共享中斷方式獲取鎖
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); // 創建SHARED模式的節點, 並加入共享隊列(AQS)末尾
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); //獲取當前節點的前序節點
if (p == head) {
int r = tryAcquireShared(arg); // 如果前序節點是head節點, 嘗試獲取鎖
if (r >= 0) {
setHeadAndPropagate(node, r); // 如果state=0, 設置頭節點並繼續喚醒隊列中的其它節點
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //阻塞線程
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
針對上面邏輯代碼, 如果state不爲0, 此時有3個線程調用了awat方法, 那麼這三個線程都會加入AQS隊列, 並處於阻塞狀態, 下面是AQS圖解
setHeadAndPropagate
這個方法的作用是把被喚醒的節點,設置成head節點, 然後喚醒隊列中的後續節點, 假如await隊列中有ThreadA -> ThreadB -> ThreadC三個等待線程, 如果ThreadA被喚醒, 且設置爲head節點, 會喚醒ThreadB節點; 如果ThreadB被喚醒且設置爲頭節點, 會喚醒ThreadC節點, 依次類推,請看下面圖解
3.3 CountDownLatch countdown源碼分析
線程調用awati方法阻塞後, 需要調用countDowan方法使得state=0,才能被再次喚醒。
//1. countDown方法入口
public void countDown() {
sync.releaseShared(1);
}
//2. Sync.releaseShared 方法釋放鎖邏輯
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 嘗試釋放共享鎖
doReleaseShared(); // 執行鎖釋放邏輯
return true;
}
return false;
}
//3. 嘗試釋放鎖
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
- 當state=0時, 直接返回false, 否則state-1, 通過CAS方式更新state, 再將更新後的state和0比較, 並返回。
- 如果state=0, 調用doReleaseShared方法進行鎖匙放
doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 喚醒線程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 如果CAS失敗, 可能是這個時候剛好有個節點入隊, 入隊時, 會將ws修改爲-1
continue; // loop on failed CAS
}
if (h == head) //說明unparkSuccessor還沒有喚醒線程, 退出循環
break;
}
}
注意共享鎖、獨佔鎖釋放的區別, tryRelease的邏輯可以查看 ReentrantLock 原理分析.md,
釋放鎖的前部分邏輯和獨佔鎖釋放邏輯一樣, 先判斷頭節點是不是signal狀態, 如果是修改爲默認狀態0, 並喚醒頭節點的下一個節點。
Node.PROPAGATE,標識PROPAGATE狀態的節點, 是共享鎖模式下的節點狀態,處於這個狀態下節點,會對線程的喚醒進行傳播
針對是否退出循環操作,
- head == h成立, 說明頭節點還沒有被unparkSuccessor喚醒線程, 此時break推出循環
- head == h不成立, unparkSuccessor喚醒的線程佔有了鎖, 會重新進入下一輪循環, 喚醒下一個線程節點
四、CyclicBarrier
4.1 CyclicBarrier知識
CyclicBarrier是循環(Cyclic)柵欄(Barrier),它的作用是讓一組線程到達屏障(Barrier, 也叫同步點)時阻塞, 直到最後一個線程到達屏障, 所有的線程才繼續執行, 類的構造方法如下,參數parties表示柵欄需要攔截的線程數量
CyclicBarrier(int parties)
線程執行await方法時, 是告訴CyclicBarrier當前線程已經到達屏障, 然後線程會被阻塞, 其實現是基於ReentrantLock和Condition組合實現的
注意點
-
CountDownLatch和CyclicBarrier之間的區別, CountDownLatch只能使用一次, CyclicBarrier可以反覆使用
-
對於計數值 parties,如果沒有足夠的線程調用 CyclicBarrier.await,則所有調用 await 的線程都會被阻塞
-
CyclicBarrier 可以調用 await(timeout, unit),設置超時時間,在設定時間內,如果沒有足夠線程到達,則解除阻塞狀態,繼續執行
-
通過 reset 重置計數,會使得進入 await 的線程出現 BrokenBarrierException
-
如果採用是 CyclicBarrier(int parties, Runnable barrierAction) 構 造 方 法 , 執行barrierAction 操作的是最後到達的線程
4.2 CyclicBarrier實踐用例
生產實踐中, 存在數據遷移操作, 在數據量比較大的時候可以創建多線程進行進行分頁批量處理, 下面是邏輯代碼
//main 入口
public class CycliBarrierMain extends Thread {
@Override
public void run() {
System.out.println("執行數據校驗邏輯, 檢查數據量是否正確");
}
public static void main(String[] args) {
//CycliBarrierMain, 其它線程執行完之後最後需要的線程
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new CycliBarrierMain()) ;
new Thread(new DataTransferThread(0, 1000, cyclicBarrier)).start();
new Thread(new DataTransferThread(1001, 2000, cyclicBarrier)).start();
new Thread(new DataTransferThread(2001, 3000, cyclicBarrier)).start();
}
}
// 數據處理線程
public class DataTransferThread extends Thread {
private CyclicBarrier cyclicBarrier ;
private int startIndex ;
private int endIndex ;
public DataTransferThread(int startIndex, int endIndex, CyclicBarrier cyclicBarrier) {
this.startIndex = startIndex ;
this.endIndex = endIndex ;
this.cyclicBarrier = cyclicBarrier ;
}
@Override
public void run() {
System.out.println("開始遷移 startIndex=" + startIndex+ " , endIndex=" +endIndex+ " 位置的數據");
try {
cyclicBarrier.await(); // 數據遷移完, 執行等待操作
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
五、Semaphore
5.1 基本作用
semaphore可以控制同時訪問的線程個數, 通過acquire獲取一個許可, 如果沒有就等待, 通過release釋放一個許可, 可以用於限流。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
參數permits用於設置state的值, 每調用一次acquire, 會執行state=state-1; 調用release的時候,會執行state=state+1; 如果acquire時state=0, 會阻塞當前線程, 等待release釋放鎖資源。
5.2 公平策略、非公平策略
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
參數fair用於決定使用公平策略還是非公平測試, 默認情況使用的是非公平策略
FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())//判斷AQS中是否有線程已經在排隊, 嚴格按照FIFO順序執行
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining)) // AQS沒有其它等待線程, 執行CAS
return remaining;
}
}
}
NonfairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
對比公平策略和非公平策略, 公平策略多了一個hasQueuedPredecessors判斷
源碼分析, 因爲獲取鎖、釋放鎖的邏輯和CountDownLatch一樣基於共享鎖的方式操作的, 代碼邏輯相同, 具體邏輯請看第三節, 這裏不在具體分析。
//獲取鎖
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//釋放鎖
public void release() {
sync.releaseShared(1);
}
5.3 實踐用例
這裏以汽車佔用車位示例
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5) ;
for (int i = 0 ; i < 10 ; i++) {
new Car(semaphore).start();
}
}
static class Car extends Thread {
private Semaphore semaphore ;
public Car(Semaphore semaphore) {
this.semaphore = semaphore ;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("線程 " + Thread.currentThread().getName() + " 佔用了停車位");
TimeUnit.SECONDS.sleep(2);
System.out.println("線程 " + Thread.currentThread().getName() + " 輛車開走了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}