1. 程式人生 > >你會這道阿裏多線程面試題嗎?

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

notify 讓我 註意 多人 通知 args 功能 code his

背景
在前幾天,群裏有個群友問了我一道面試阿裏的時候遇到的多線程題目,這個題目比較有意思,在這裏和大家分享一下。

廢話不多說,直接上題目:

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

一些經常刷面試題的朋友,之前肯定遇到過下面這個題目:

兩個線程交替打印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用來控制我們線程的執行,從而打印出我們目標的結果

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塊包裹住,那麽喚醒的線程會做會搶奪同步鎖。

這個老司機的代碼的確能跑通,但是有一個問題是什麽呢?當我們線程數很大的時候,由於我們不確定喚醒的線程到底是否是下一個要執行的就有可能會出現搶到了鎖但不該自己執行,然後又進入wait的情況,比如現在有100個線程,現在是第一個線程在執行,他執行完之後需要第二個線程執行,但是第100個線程搶到了,發現不是自己然後又進入wait,然後第99個線程搶到了,發現不是自己然後又進入wait,然後第98,97...直到第3個線程都搶到了,最後才到第二個線程搶到同步鎖,這裏就會白白的多執行很多過程,雖然最後能完成目標。
技術分享圖片
還有其他老司機用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();
        }
    }

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

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