華爲和阿里都考過的多線程編程題,你會嗎?多線程交替打印 ABC的多種實現方法

題目描述以下:java

編寫一個程序,開啓三個線程,這三個線程的 ID 分別是 A、B 和 C,每一個線程把本身的 ID 在屏幕上打印 10 遍,要求輸出結果必須按 ABC 的順序顯示,如 ABCABCABC... 依次遞推

這是一道經典的多線程編程面試題,首先吐槽一下,這道題的需求非常奇葩,先開啓多線程,而後再串行打印 ABC,這不是吃飽了撐的嗎?不過既然是道面試題,就無論這些了,其目的在於考察你的多線程編程基礎。就這道題,你要是寫不出個三四種解法,你都很差意思說你學過多線程。哈哈開玩笑,下面就爲你介紹一下本題的幾種解法。面試

一、最簡單的方法——使用 LockSupport

LockSupport 是java.util.concurrent.locks包下的工具類,它的靜態方法unpark()park()能夠分別實現阻塞當前線程和喚醒指定線程的效果,因此用它解決這樣的問題簡直是小菜一碟,代碼以下:shell

public class PrintABC {
  
    static Thread threadA, threadB, threadC;
  
      public static void main(String[] args) {
        threadA = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 打印當前線程名稱
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadB);
                // 當前線程阻塞
                LockSupport.park();
            }
        }, "A");
        threadB = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 先阻塞等待被喚醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadC);
            }
        }, "B");
        threadC = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 先阻塞等待被喚醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadA);
            }
        }, "C");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

執行結果以下:編程

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

二、最傳統的方法——使用synchronized 鎖機制

這種方法就是直接使用 Java 的 synchronized 關鍵字,配合 Object 的 wait()notifyAll()方法實現線程交替打印的效果,不過這種寫法的複雜度和代碼量都偏大。因爲notify()notifyAll()方法都不能喚醒指定的線程,因此須要三個布爾變量對線程執行順序進行控制。另外要注意的就是,for 循環中的 i++須要在線程打印以後執行,不然每次被喚醒後,不論是不是輪到當前線程打印都會執行i++,這顯然不是咱們想要的。代碼以下 (通常B、C線程和A線程的執行邏輯相似,只在A線程代碼中進行詳細註釋說明):多線程

public class PrintABC {
      // 使用布爾變量對打印順序進行控制,true表示輪到當前線程打印
    private static boolean startA = true;
    private static boolean startB = false;
    private static boolean startC = false;

    public static void main(String[] args) {
        // 做爲鎖對象
        final Object o = new Object();
        // A線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startA) {
                        // 表明輪到當前線程打印
                        System.out.print(Thread.currentThread().getName());
                        // 下一個輪到B打印,因此把startB置爲true,其它爲false
                        startA = false;
                        startB = true;
                        startC = false;
                        // 喚醒其餘線程
                        o.notifyAll();
                        // 在這裏對i進行增長操做
                        i++;
                    } else {
                        // 說明沒有輪到當前線程打印,繼續wait
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "A").start();
        // B線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startB) {
                        System.out.print(Thread.currentThread().getName());
                        startA = false;
                        startB = false;
                        startC = true;
                        o.notifyAll();
                        i++;
                    } else {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "B").start();
        // C線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startC) {
                        System.out.print(Thread.currentThread().getName());
                        startA = true;
                        startB = false;
                        startC = false;
                        o.notifyAll();
                        i++;
                    } else {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "C").start();
    }
}

執行結果以下:併發

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

三、使用 Lock 搭配 Condition 實現

使用 synchronized 鎖機制的寫法着實有些複雜,何不試試 ReentrantLock?這是java.util.concurrent.locks包下的鎖實現類,它擁有更靈活的 API,可以對多線程執行流程實現更精細的控制,特別是在搭配 Condition 使用的狀況下,能夠爲所欲爲地控制多個線程的執行順序,來看看這個組合在本題中的使用吧,代碼以下:工具

public class PrintABC {
  public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        // 使用ReentrantLock的newCondition()方法建立三個Condition
        // 分別對應A、B、C三個線程
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();

        // A線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    // 叫醒B線程
                    conditionB.signal();
                    // 本線程阻塞
                    conditionA.await();
                }
                // 這裏有個坑,要記得在循環以後調用signal(),不然線程可能會一直處於
                // wait狀態,致使程序沒法結束
                conditionB.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 在finally代碼塊調用unlock方法
                lock.unlock();
            }
        }, "A").start();
        // B線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    conditionC.signal();
                    conditionB.await();
                }
                conditionC.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "B").start();
        // C線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    conditionA.signal();
                    conditionC.await();
                }
                conditionA.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "C").start();
    }
}

執行結果以下:ui

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

四、使用 Semaphore 實現

semaphore中文意思是信號量,本來是操做系統中的概念,JUC下也有個 Semaphore 的類,可用於控制併發線程的數量。Semaphore 的構造方法有個 int 類型的 permits 參數,以下:操作系統

public Semaphore(int permits) {...}

其中 permits 指的是該 Semaphore 對象可分配的許可數,一個線程中的 Semaphore 對象調用acquire()方法可讓線程獲取許可繼續運行,同時該對象的許可數減一,若是當前沒有可用許可,線程會阻塞。該 Semaphore 對象調用release()方法能夠釋放許可,同時其許可數加一。Talk is cheap, show me the code!線程

public class PrintABC {
  
      public static void main(String[] args) {
        // 初始化許可數爲1,A線程能夠先執行
        Semaphore semaphoreA  = new Semaphore(1);
        // 初始化許可數爲0,B線程阻塞
        Semaphore semaphoreB  = new Semaphore(0);
        // 初始化許可數爲0,C線程阻塞
        Semaphore semaphoreC  = new Semaphore(0);

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    // A線程得到許可,同時semaphoreA的許可數減爲0,進入下一次循環時
                    // A線程會阻塞,知道其餘線程執行semaphoreA.release();
                    semaphoreA.acquire();
                    // 打印當前線程名稱
                    System.out.print(Thread.currentThread().getName());
                    // semaphoreB許可數加1
                    semaphoreB.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    semaphoreB.acquire();
                    System.out.print(Thread.currentThread().getName());
                    semaphoreC.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    semaphoreC.acquire();
                    System.out.print(Thread.currentThread().getName());
                    semaphoreA.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
    }
}

執行結果以下:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

五、總結

本文一共介紹了四種三個線程交替打印的實現方法,其中第一種方法最簡單易懂,可是更能考察多線程編程功底的應該是第二和第三種方法,在面試中也更加分。只要把這幾種方法熟練掌握並完全理解,之後碰到此類題型就不用慌了。

相關文章
相關標籤/搜索