你會這道阿里多線程面試題嗎?

背景

在前幾天,羣裏有個羣友問了我一道面試阿里的時候遇到的多線程題目,這個題目比較有意思,在這裏和你們分享一下。java

廢話很少說,直接上題目:git

經過N個線程順序循環打印從0至100,如給定N=3則輸出:
thread0: 0
thread1: 1
thread2: 2
thread0: 3
thread1: 4
.....

一些常常刷面試題的朋友,以前確定遇到過下面這個題目:github

兩個線程交替打印0~100的奇偶數:
偶線程:0
奇線程:1
偶線程:2
奇線程:3

這兩個題目看起來類似,第二個題目稍微來講比較簡單一點,你們能夠先思考一下兩個線程奇偶數如何打印。面試

兩線程奇偶數打印

有一些人這裏可能會用討巧的,用一個線程進行循環,在每次循環裏面都會作是奇數仍是偶數的判斷,而後打印出這個咱們想要的結果。在這裏咱們不過多討論這種違背題目本意的作法。數組

其實要作這個題目咱們就須要控制兩個線程的執行順序,偶線程執行完以後奇數線程執行,這個有點像通知機制,偶線程通知奇線程,奇線程再通知偶線程。而一看到通知/等待,立馬就有朋友想到了Object中的wait和notify。沒錯,這裏咱們用wait和notify對其進行實現,代碼以下:多線程

public class 交替打印奇偶數 {
    static class SoulutionTask implements Runnable{
        static int value = 0;
        @Override
        public void run() {
            while (value <= 100){
                synchronized (SoulutionTask.class){
                    System.out.println(Thread.currentThread().getName() + ":" + value++);
                    SoulutionTask.class.notify();
                    try {
                        SoulutionTask.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new SoulutionTask(), "偶數").start();
        new Thread(new SoulutionTask(), "奇數").start();
    }
}

這裏咱們有兩個線程,經過notify和wait用來控制咱們線程的執行,從而打印出咱們目標的結果ide

N個線程循環打印

再回到咱們最初的問題來,N個線程進行循環打印,這個問題我再幫助羣友解答了以後,又再次把這個問題在羣裏面拋了出來,很多老司機以前看過交替打印奇偶數這道題目,因而立刻作出了幾個版本,讓咱們看看老司機1的代碼:學習

public class 老司機1 implements Runnable {

    private static final Object LOCK = new Object();
    /**
     * 當前即將打印的數字
     */
    private static int current = 0;
    /**
     * 當前線程編號,從0開始
     */
    private int threadNo;
    /**
     * 線程數量
     */
    private int threadCount;
    /**
     * 打印的最大數值
     */
    private int maxInt;

    public 老司機1(int threadNo, int threadCount, int maxInt) {
        this.threadNo = threadNo;
        this.threadCount = threadCount;
        this.maxInt = maxInt;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (LOCK) {
                // 判斷是否輪到當前線程執行
                while (current % threadCount != threadNo) {
                    if (current > maxInt) {
                        break;
                    }
                    try {
                        // 若是不是,則當前線程進入wait
                        LOCK.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                // 最大值跳出循環
                if (current > maxInt) {
                    break;
                }
                System.out.println("thread" + threadNo + " : " + current);
                current++;
                // 喚醒其餘wait線程
                LOCK.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        int threadCount = 3;
        int max = 100;
        for (int i = 0; i < threadCount; i++) {
            new Thread(new 老司機1(i, threadCount, max)).start();
        }
    }
}

核心方法在run裏面,能夠看見和咱們交替打印奇偶數原理差很少,這裏將咱們的notify改爲了notifyAll,這裏要注意一下不少人會將notifyAll理解成其餘wait的線程所有都會執行,實際上是錯誤的。這裏只會將wait的線程解除當前wait狀態,也叫做喚醒,因爲咱們這裏用同步鎖synchronized塊包裹住,那麼喚醒的線程會作會搶奪同步鎖。ui

這個老司機的代碼的確能跑通,可是有一個問題是什麼呢?當咱們線程數很大的時候,因爲咱們不肯定喚醒的線程究竟是否是下一個要執行的就有可能會出現搶到了鎖但不應本身執行,而後又進入wait的狀況,好比如今有100個線程,如今是第一個線程在執行,他執行完以後須要第二個線程執行,可是第100個線程搶到了,發現不是本身而後又進入wait,而後第99個線程搶到了,發現不是本身而後又進入wait,而後第98,97...直到第3個線程都搶到了,最後纔到第二個線程搶到同步鎖,這裏就會白白的多執行不少過程,雖然最後能完成目標。this

還有其餘老司機用lock/condition也實現了這樣的功能,還有老司機用比較新穎的方法好比隊列去作,固然這裏就很少提了,大體的原理都是基於上面的,這裏我說一下個人作法,在Java的多線程中提供了一些經常使用的同步器,在這個場景下比較適合於使用Semaphore,也就是信號量,咱們上一個線程持有下一個線程的信號量,經過一個信號量數組將所有關聯起來,代碼以下:

static int result = 0;
    public static void main(String[] args) throws InterruptedException {
        int N = 3;
        Thread[] threads = new Thread[N];
        final Semaphore[] syncObjects = new Semaphore[N];
        for (int i = 0; i < N; i++) {
            syncObjects[i] = new Semaphore(1);
            if (i != N-1){
                syncObjects[i].acquire();
            }
        }
        for (int i = 0; i < N; i++) {
            final Semaphore lastSemphore = i == 0 ? syncObjects[N - 1] : syncObjects[i - 1];
            final Semaphore curSemphore = syncObjects[i];
            final int index = i;
            threads[i] = new Thread(new Runnable() {

                public void run() {
                    try {
                        while (true) {
                            lastSemphore.acquire();
                            System.out.println("thread" + index + ": " + result++);
                            if (result > 100){
                                System.exit(0);
                            }
                            curSemphore.release();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            });
            threads[i].start();
        }
    }

經過這種方式,咱們就不會有白白喚醒的線程,每個線程都按照咱們所約定的順序去執行,這其實也是面試官所須要考的地方,讓每一個線程的執行都能再你手中獲得控制,這也能夠驗證你多線程知識是否牢固。

最後這篇文章被我收錄於JGrowing-Java面試篇,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:https://github.com/javagrowing/JGrowing 麻煩給個小星星喲。

若是你們以爲這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章
相關標籤/搜索