《今天面試了嗎》- 併發編程之AQS同步工具類

點擊上方 匠心Java選擇 設爲星標

優質文章,及時送達


前言

上次面試中問到AQS簡直不要太痛苦,全是問的源碼。但是源碼有時間還是要看看的,畢竟對於提升我們的寫代碼的能力還是有幫助的。今天的面試緊接上回的AQS,內容是基於AQS實現的四大併發工具類:CyclicBarrier,CountDownLatch,Semaphore和Exchanger,簡要分析實現原理,着重講述如何使用。

面試環節

面試官:上次聊到AQS,你在開發過程中用過AQS的幾個工具類嗎?比如CyclicBarrier...

我: 用過,CyclicBarrier是一個同步輔助類。它允許一組線程互相等待,直到到達某個公共屏障點。在涉及一組固定大小的線程的程序裏,這些線程必須不時的互相等待,此時CyclicBarrier 很有用。因爲CyclicBarrier在釋放等待線程後可以重用,因此成爲循環的屏障。 下面來看下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內部是使用重入鎖和Condition的。它有兩個構造函數:
/**
    創建一個新的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方法,用於將該屏障置爲初始狀態。

面試官: 那CyclicBarrier什麼場景下用呢?
我: CyclicBarrier適用於多線程合併的操作,用於多線程計算數據,最後合併計算結果的應用場景。舉個例子:
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;
            }
        }
    }
CountDownLatch內部通過共享鎖實現:
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嗎?

我: 信號量Semaphore是一個控制訪問多個共享資源的計數器,和CountDownLatch一樣,其本質上是一個“共享鎖”。在API是這麼介紹信號量的:一個計數信號量,從概念上講,信號量維護了一個許可集。
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是一個同步器,字面上就可以看出這個類的主要作用是交換數據。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();
    }
}

運行結果:



-END-

如果看到這裏,說明你喜歡這篇文章,請 轉發、點贊。同時 標星(置頂)本公衆號可以第一時間接受到博文推送。

喜歡文章,點個在看 

本文分享自微信公衆號 - 匠心Java(code-the-world)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章