參考:用CountDownLatch提升請求處理速度,CountDownLatch的理解和使用
1.使用場景
看了網上的一些解釋,這邊直接摘取一下過來:
2.代碼示例
代碼使用的實例:
package huangzj.springmvc.controller;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
private CountDownLatch countDownLatch;
private int start = 10;
private int mid = 100;
private int end = 200;
private volatile int tmpRes1, tmpRes2;
private int add(int start, int end) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
private int sum(int a, int b) {
return a + b;
}
public void calculate() {
countDownLatch = new CountDownLatch(2);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 確保線程3先與1,2執行,由於countDownLatch計數不爲0而阻塞
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " : 開始執行");
tmpRes1 = CountDownLatchDemo.this.add(start, mid);
System.out.println(Thread.currentThread().getName() +
" : calculate ans: " + tmpRes1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}, "線程1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 確保線程3先與1,2執行,由於countDownLatch計數不爲0而阻塞
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " : 開始執行");
tmpRes2 = CountDownLatchDemo.this.add(mid + 1, end);
System.out.println(Thread.currentThread().getName() +
" : calculate ans: " + tmpRes2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}, "線程2");
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " : 開始執行");
countDownLatch.await();
int ans = CountDownLatchDemo.this.sum(tmpRes1, tmpRes2);
System.out.println(Thread.currentThread().getName() +
" : calculate ans: " + ans);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "線程3");
thread3.start();
thread1.start();
thread2.start();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo demo = new CountDownLatchDemo();
demo.calculate();
Thread.sleep(1000);
}
}
結果如下,在await之前的線程運行結束之後纔會執行後面的線程:
3.代碼閱讀
1.構造方法
構造方法的入參表示,需要執行多少次線程之後,才進行釋放,後面的線程才能運行
public CountDownLatch(int count) {
//這邊的入參解釋,count表示執行一定次數之後線程才能放過.
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
2.countDown
一個countDown表示一次共享鎖的釋放:
public void countDown() {
//釋放一次鎖.如果數量爲0則表示等待的線程都執行完畢了
sync.releaseShared(1);
}
releaseShared是AQS提供的。
public final boolean releaseShared(int arg) {
//如果嘗試釋放鎖成功
if (tryReleaseShared(arg)) {
//釋放鎖,修改參數
doReleaseShared();
return true;
}
return false;
}
我們主要來看一下tryReleaseShared這個方法是怎麼釋放的
如果狀態本來就是0說明不需要釋放,直接返回false,否則通過釋放state成功返回true,則進入上面的doReleaseShared修改其他線程的waitState,告知鎖釋放.
/**
* 這邊的實現就是死循環通過CAS的方式去釋放state
*/
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;
}
}
}
3.await
await方法調用會造成當state不等於0其他線程進行等待,看這邊的註釋
public void await() throws InterruptedException {
//造成當前線程等待countDown計數降爲0,除非線程本來就是中斷狀態
sync.acquireSharedInterruptibly(1);
}
這邊如果線程是中斷狀態,再進行中斷直接報錯.
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果線程被中斷報錯
if (Thread.interrupted()){
throw new InterruptedException();
}
//嘗試獲取共享鎖
if (tryAcquireShared(arg) < 0){
doAcquireSharedInterruptibly(arg);
}
}
tryAcquireShared這邊判斷是否獲取鎖成功的條件就是state是不是爲0,這和我們上面說的一致,當countDown沒有降到0的時候,其他線程是不能進行操作的。
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
這邊其實我們看到會往隊列裏面加一個共享節點,然後進入等待獲取鎖,直到獲取成功爲止。
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);
// help GC
p.next = null;
failed = false;
return;
}
}
//進入隊列之後進行休眠,park,休眠之後如果判斷線程狀態是intercept,直接拋錯
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
throw new InterruptedException();
}
}
} finally {
if (failed){
cancelAcquire(node);
}
}
}
4.我的理解
CountDownLatch之所以可以實現在await之前的線程執行結束才執行後面的線程,我的想法的線程是有在隊列和不在隊列的區分的,線程調用countDown方法表示該線程一定是在await之前去執行的。而調用await方法的線程一定是在countDown直到降爲0才能進行運行的。
舉個例子就是說:現在有線程A、B、C、D,AB運行結束之後產生結果,CD才能使用他們帶來的結果進行計算。則在AB的運行結束之後需要釋放計數器(-1操作)就是調用countDown方法。
而CD需要進行等待,在CD一開始就應該調用await方法,進入隊列裏面去等待,這樣才能保證在AB運行的期間,CD不會去搶佔線程獲取到AB的運行權限。
我們用一張圖來解釋這個過程: