一、概述
在高併發的情況下,java提供了concurrent包,來滿足部分特定併發場景需求,無論是重複造輪子、或是有技術需求,都可以瞭解下
concurrent中的兩個同步工具類:
CountDownLatch(計數器)
CyclicBarrier(迴圈柵欄)
二、使用場景及實現
2.1、CountDownLatch簡介 CountDownLatch是一個同步工具類,它允許一個或多個執行緒一直等待,直到其他執行緒的操作執行完後再執行。例如,應用程式的主執行緒希望在負責啟動框架服務的執行緒已經啟動所有的框架服務之後再執行
關鍵詞:
計數器、aqs
2.2、CountDownLatch使用場景
2.3、CountDownLatch實現
CountDownLatch基於AQS(佇列同步器),底層全部使用cas實現
cas一分鐘: CAS是一個原子操作,在本地方法(JNI)出現後,java可以利用處理器提供的指令保證併發場景的同步,大概流程是:比較一個記憶體位置的值並且只有相等時修改這個記憶體位置的值為新的值,保證了新的值總是基於最新的資訊計算的,如果有其他執行緒在這期間修改了這個值則CAS失敗
AQS實現:
內建佇列
CANCELLED 取消狀態 ==1 由於在同步佇列中等待的執行緒等待超時或被中斷,需要從同步佇列中取消等待,節點進入該狀態將不會變化
SIGNAL 等待觸發狀態(前節點狀態為SIGNAL,當前節點才能掛起) ==-1 後繼節點的執行緒處於等待狀態
CONDITION 等待條件狀態 ==-2 節點在等待佇列中
PROPAGATE 狀態需要向後傳播 ==-3 表示下一次共享式同步狀態獲取將會無條件被傳播下去
2.3.1執行緒獲取鎖過程:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { //佇列已經初始化且存在多個節點
node.prev = pred; //將當前node的上一節點指向tail
if (compareAndSetTail(pred, node)) { //設定tail為當前節點
pred.next = node; //佇列插入成功返回當前節點
return node;
}
}
enq(node); //如果tail節點為空,說明佇列還未初始化、進入enq 或者在當前node入隊時前節點已出隊也進入enq
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //初始化時則將head節點指向一個空節點,代表已執行cas的B執行緒
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
複製程式碼
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //是否刪除節點
try {
boolean interrupted = false; //是否中斷狀態
for (;;) { //進入自旋
final Node p = node.predecessor(); //判斷前一個節點是否是head節點
if (p == head && tryAcquire(arg)) { //如果是,則嘗試再次獲取鎖
setHead(node); //設定head為當前node節點
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //如果pred的waitStatus為Node.SIGNAL,則返回true通過LockSupport.park()方法把執行緒A掛起,並等待被喚醒
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //如果pred的waitStatus > 0,表明pred的執行緒狀態CANCELLED,需從佇列中刪除
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; //上個節點的上個節點》》pred(上上個節點)》當前節點的上個節點
} while (pred.waitStatus > 0);
pred.next = node; //上上個節點的下個節點為當前節點
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //如果pred的waitStatus == 0,則通過CAS指令修改waitStatus為Node.SIGNAL
}
return false;
}
複製程式碼
2.3.2釋放鎖過程:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //如果頭結點head的waitStatus值為-1,則用CAS指令重置為0;
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //找到waitStatus值小於0的節點s,通過LockSupport.unpark(s.thread)喚醒執行緒
LockSupport.unpark(s.thread);
}
複製程式碼
CountDownLatch包裝
主執行緒執行await方法,tryAcquireShared方法中如果state不等於0,返回-1,則加入到等待佇列中,主執行緒通過LockSupport.park(this)被掛起。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
如果state為0,通過LockSupport.unpark喚醒await方法中掛起的主執行緒。
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))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
複製程式碼
2.4 CyclicBarrier的使用場景
`public static final int INIT_SIZE = 4;
private static CyclicBarrier barrier;
public static void main(String[] args) {
System.out.println("開啟CyclicBarrier");
//初始化CyclicBarrier
barrier = new CyclicBarrier(INIT_SIZE, new Runnable() {
public void run() {
System.out.println("所有執行緒都就緒了");
}
});
//開啟3個執行緒
for (int i=0;i<INIT_SIZE;i++){
new ThreadDemo().start();
}
}
static class ThreadDemo extends Thread {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"執行緒就緒");
barrier.await();
System.out.println(Thread.currentThread().getName()+"執行緒繼續執行");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
複製程式碼
開啟CyclicBarrier
Thread-0執行緒就緒
Thread-2執行緒就緒
Thread-1執行緒就緒
所有執行緒都就緒了
Thread-1執行緒繼續執行
Thread-0執行緒繼續執行
Thread-2執行緒繼續執行
複製程式碼
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;//執行緒數初始化
this.count = parties;//剩餘未到達柵欄執行緒數
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
複製程式碼
2.5 CyclicBarrier的實現
關鍵詞:ReentrantLock、Condition
ReentrantLock 一分鐘: 可重入鎖,有兩種獲取鎖方式:公平鎖和非公平鎖; 公平鎖:統一執行緒可以多次獲取鎖,state相應+1,其他執行緒進入佇列排隊,直到state=0釋放鎖 非公平鎖:新執行緒可直接嘗試競爭鎖,不進入等待佇列
Condition :一分鐘 Condition是同步器的內部類,提供了類似Object監視器的方法,通過與Lock配合來實現等待/通知模式
signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); //獲取ReentrantLock互斥鎖
try {
final Generation g = generation;//獲取generation物件
if (g.broken)//如果generation損壞,丟擲異常
throw new BrokenBarrierException();
if (Thread.interrupted()) {
//如果當前執行緒被中斷,則呼叫breakBarrier方法(將損壞狀態設定為 true),停止CyclicBarrier,並喚醒所有執行緒
breakBarrier();
throw new InterruptedException();
}
int index = --count; // 減count
//index=0,也就是說,有0個執行緒未滿足CyclicBarrier條件,也就是條件滿足,
//可以喚醒所有的執行緒了
if (index == 0) { // tripped
boolean ranAction = false;
try {
//這就是構造器的第二個引數,如果不為空的話,就執行這個Runnable的run方法,注意是最後一個執行await操作的執行緒執行的這個run方法。
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//喚醒所有的等待執行緒
nextGeneration();
return 0;
} finally {
if (!ranAction)//如果執行柵欄任務的時候失敗了,就將柵欄失效,並保證一定能喚醒所有執行緒
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
//當barrier的條件滿足、當前執行緒被中斷或者已經超時才會跳出迴圈
for (;;) {
try {
if (!timed)//如果沒有指定超時引數,則直接呼叫Condition的await方法。
trip.await();
else if (nanos > 0L)//否則根據超時時間呼叫awaitNanos方法
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//執行過程中,執行緒被中斷的話,改變generation狀態
if (g == generation && ! g.broken) {
breakBarrier();//執行breakBarrier,喚醒所有執行緒
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)//如果當前generation已經損壞,丟擲異常
throw new BrokenBarrierException();
if (g != generation)//如果generation已經更新換代,則返回當前index跳出
return index;
//如果是引數是超時等待並已經超時,則執行breakBarrier()方法喚醒所有等待執行緒
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
複製程式碼
三、CountDownLatch、CyclicBarrier區別
-
CyclicBarrier是多個執行緒互相等待,直到到達同一個同步點,再繼續一起執行(CountDownLatch可以線上程中執行await來實現CyclicBarrier)
-
CountDownLatch 的缺點是不能重複使用,CyclicBarrier.reset 能重置柵欄,此時正在等待的執行緒會收到 BrokenBarrierException