點擊上方 匠心Java,選擇 設爲星標
優質文章,及時送達
前言
面試環節
面試官:上次聊到AQS,你在開發過程中用過AQS的幾個工具類嗎?比如CyclicBarrier...
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
//parties變量表示攔截線程的總數量,count變量表示攔截線程的剩餘需要數量
private final int parties;
//barrierCommand變量爲CyclicBarrier接收的Runnable命令,用於在線程到達屏障時,優先執行barrierCommand,用於處理更加複雜的業務場景。
private final Runnable barrierCommand;
//generation變量表示CyclicBarrier的更新換代
private Generation generation = new Generation();
/**
創建一個新的CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,並在啓動barrier時執行給定的屏障操作,該操作由最後一個進入barrier的線程執行。
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
/**
創建一個新的CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,但它不會在啓動barrier時執行預定義的操作。
*/
public CyclicBarrier(int parties) {
this(parties, null);
}
面試官:那CyclicBarrier是怎麼讓線程到達屏障後處於等待狀態的呢?
我:使用await()方法,每個線程調用await()方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。當所有線程都到達了屏障,結束阻塞,所有線程可繼續執行後續邏輯。
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
//獲取鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//分代
final Generation g = generation;
//當前generation已損壞,拋出BrokenBarrierException異常
if (g.broken)
throw new BrokenBarrierException();
//如果線程中斷,終止CyclicBarrier
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//進來一個線程,count-1
int index = --count;
//如果count==0表示所有線程均已到達屏障,可以觸發barrierCommand任務
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//喚醒所有等待線程,並更新generation
nextGeneration();
return 0;
} finally {
//如果barrierCommand執行失敗,終止CyclicBarrier
if (!ranAction)
breakBarrier();
}
}
for (;;) {
try {
//如果不是超時等待,則調用Condition.await()方法等待
if (!timed)
trip.await();
else if (nanos > 0L)
//如果是超時等待,則調用Condition.awaitNanos()等待
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
//generation已經更新,返回Index
if (g != generation)
return index;
//超時等待並且時間已經到了,終止CyclicBarrier,並拋出超時異常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//釋放鎖
lock.unlock();
}
1、最後一個到達:即index=0
2、超出了等待時間。
3、其他的某個線程中斷當前線程。
4、其他某個線程中斷另一個等待的線程。
5、其他某個線程在等待barrier超時。
6、其他某個線程在此barrier調用reset方法,用於將該屏障置爲初始狀態。
public class CyclicBarrierTest {
private static CyclicBarrier cyclicBarrier;
private static final Integer THREAD_COUNT = 10;
static class CyclicBarrierThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到教室了");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String [] args) {
cyclicBarrier = new CyclicBarrier(THREAD_COUNT, new Runnable() {
@Override
public void run() {
System.out.println("同學們都到齊了,開始上課吧...");
}
});
for (int i=0; i< THREAD_COUNT; i++) {
Thread thread = new Thread(new CyclicBarrierThread());
thread.start();
}
}
}
面試官:有一個和CyclicBarrier類似的工具類叫CountDownLatch,你能說下嗎?
我:CyclicBarrier描述的是“允許一組線程相互等待,直到到達某個公共屏障點,纔會進行後續任務”,而CountDownLatch所描述的是“在完成一組正在其他線程中執行的操作之前,它允許 一個或多個線程一直等待”。在API中是這樣描述的:用給定的計數初始化CountDownLatch。由於調用了countDown方法,所以在當前計數到達零之前,await方法會一直受阻塞。之後,會釋放 所有等待的線程,await的所有後續調用都將立即返回。這種現象只出現一次(計數無法被重置。如果需要重置計數,請考慮使用CyclicBarrier)。
CountDownLatch是通過一個計數器來實現的,當我們在new一個CountDownLatch對象的時候,需要傳入計數器的值,該值表示線程的數量。每當一個線程完成自己的任務後,計數器的值就會 減一。當計數器的值變爲0時,就表示所有線程均已完成任務,然後就可以恢復等待的線程繼續執行了。
CountDownLatch和CyclicBarrier還是有一點區別的:
1、CountDownLatch的作用是允許1或多個線程等待其他線程完成執行;而CyclicBarrier則是允許多個線程互相等待。
2、CountDownLatch的計數器無法被重置。CyclicBarrier的計數器可以被重置後使用。
面試官:你能說下CountDownlatch是怎麼實現的嗎?
我:CountDownlatch內部依賴Sync實現,而Sync繼承AQS。如下圖:
CountDownlatch僅提供了一個構造方法,如下:
public class CyclicBarrierTest {
private static CyclicBarrier cyclicBarrier;
private static final Integer THREAD_COUNT = 10;
static class CyclicBarrierThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到教室了");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String [] args) {
cyclicBarrier = new CyclicBarrier(THREAD_COUNT, new Runnable() {
@Override
public void run() {
System.out.println("同學們都到齊了,開始上課吧...");
}
});
for (int i=0; i< THREAD_COUNT; i++) {
Thread thread = new Thread(new CyclicBarrierThread());
thread.start();
}
}
}
再來看看Sync,是CountDownlatch的一個內部類。
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
//獲取同步狀態
int getCount() {
return getState();
}
//嘗試獲取同步狀態
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
//嘗試釋放同步狀態
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
1、在創建CountDownLatch實例時,需要傳遞一個int型參數:count,該參數爲計數器的初始值,也可以理解爲該共享鎖可以獲取的總次數。
2、當某個線程調用await()方法,程序首先判斷count的值是否爲0,如果不爲0的話,則會一直等待直到爲0爲止。
3、當其他線程調用countDown()方法時,則執行釋放共享鎖狀態,使count-1。
4、注意CountDownLatch不能回滾重置。
面試官:那你說下CountDownLatch是怎麼用的?
1、CountDownlatch提供了await()方法,來使當前線程在鎖存器遞減倒數至0以前一直等待,除非線程被中斷,當前線程可以是我們的一個主線程。2、CountDownlatch提供了countDown()方法,在子線程執行完後進行操作,遞減鎖存器的計數,如果計數到達0,則喚醒所有等待的線程(我們的主線程)。說完我拿起筆刷刷的寫起來:
public class CountDownLatchTest {
private static final Integer STUDENT_COUNT = 10;
private static CountDownLatch countDownLatch = new CountDownLatch(STUDENT_COUNT);
static class TeacherThread implements Runnable {
@Override
public void run() {
System.out.println("老師來了,等"+ STUDENT_COUNT+"位同學都到教室了纔開始上課");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(STUDENT_COUNT+"位同學都到齊了,開始上課!");
}
}
static class StudentThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"進了教室");
countDownLatch.countDown();
}
}
public static void main(String [] args) {
Thread teacher = new Thread(new TeacherThread());
teacher.start();
for (int i=0; i<STUDENT_COUNT; i++) {
Thread student = new Thread(new StudentThread());
student.start();
}
}
}
面試官:很好。懂得活學活用。你瞭解信號量Semaphore嗎?
1、如有必要,在許可可用前會阻塞每一個acquire,然後再獲取該許可。
2、每個release添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採取相應的行動。
下面以一個停車場的例子來闡述Semaphore:
1、假設停車場有5個停車位,一開始車位都空着,然後先後來了三輛車,車位夠,安排進去停車,然後又來三輛,這個時候由於只有兩個車位,所以只能停兩輛,有一輛需要在外面候着,直到 停車場有空位。
2、從程序角度講,停車場就相當於信號量Semaphore,其中許可數爲5,車輛相當於線程,當來一輛車,許可數就會減1。當停車場沒車位了(許可數==0),其他來的車輛必須等待。如果 有一輛車開車停車場,則許可數+1,然後放進來一輛車。
從上面的分析可以看出:信號量Semaphore是一個非負整數(>=1)。當一個線程想要訪問某個共享資源時,它必須先獲取Semaphore。當Semaphore>0時,獲取該資源並使Semaphore-1。如果Semaphore的值==0,則表示全部的共享資源已經被線程全部佔用,新來的線程必須等待其他線程釋放資源。當線程釋放資源時,Semaphore則+1。
面試官:你能用Semaphore實現這個停車的例子嗎?
我(又刷刷的寫起來)
public class SemaphoreTest {
static class Parking {
private Semaphore semaphore;
Parking(int count) {
semaphore = new Semaphore(count);
}
public void park() {
try {
//獲取信號量
semaphore.acquire();
long time = (long) (Math.random()*10+1);
System.out.println(Thread.currentThread().getName()+"進入停車場停車,停車時間:"+time+"秒");
//模擬停車時間
Thread.sleep(time);
System.out.println(Thread.currentThread().getName()+"開出停車場...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放信號量(跟lock的用法差不多)
semaphore.release();
}
}
}
static class Car implements Runnable{
private Parking parking;
Car(Parking parking) {
this.parking = parking;
}
/**
* 每輛車相當於一個線程,線程的任務就是停車
*/
@Override
public void run() {
parking.park();
}
}
public static void main(String [] args) {
//假設有3個停車位
Parking parking = new Parking(3);
//這時候同時來了5輛車,只有3輛車可以進去停車,其餘2輛車需要等待有空餘車位之後才能進去停車。
for (int i=0; i<5; i++) {
Thread thread = new Thread(new Car(parking));
thread.start();
}
}
}
運行結果:
我:Exchanger是一個同步器,字面上就可以看出這個類的主要作用是交換數據。Exchanger有點類似CyclicBarrier,前面說到CyclicBarrier是一個柵欄,到達柵欄的 線程需要等待一定數量的線程到達後,才能通過柵欄。Exchanger可以看成是一個雙向的柵欄。線程1到達柵欄後,會首先觀察有沒有其他線程已經到達柵欄,如果沒有就會等待。如果已經有其他線程(比如線程2)到達了,就會以成對的方式交換各自攜帶的信息,因此Exchanger非常適合兩個線程之間的數據交換。 如下圖:
面試官:那你能跟我舉個例子說下Exchanger怎麼用嗎?
public class ExchangerTest {
static class ThreadA implements Runnable {
private Exchanger<String> exchanger;
ThreadA (Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
try {
//模擬業務代碼
Long time = (long)(Math.random()*10+1)*10;
System.out.println("線程A等待了"+time+"秒");
Thread.sleep(time);
//線程間數據交換
System.out.println("在線程A得到線程B的值:"+ exchanger.exchange("我是線程A"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ThreadB implements Runnable {
private Exchanger<String> exchanger;
ThreadB(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
try {
//模擬業務代碼
Long time = (long)(Math.random()*10+1)*10;
System.out.println("線程B等待了"+time+"秒");
Thread.sleep(time);
//線程間數據交換
System.out.println("在線程B得到線程A的值:"+ exchanger.exchange("我是線程B"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String [] args) {
Exchanger<String> exchanger = new Exchanger<>();
//線程A和線程B要使用同一個exchanger纔有用
Thread threadA = new Thread(new ThreadA(exchanger));
Thread threadB = new Thread(new ThreadB(exchanger));
threadA.start();
threadB.start();
}
}
運行結果:
如果看到這裏,說明你喜歡這篇文章,請 轉發、點贊。同時 標星(置頂)本公衆號可以第一時間接受到博文推送。
喜歡文章,點個在看
本文分享自微信公衆號 - 匠心Java(code-the-world)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。