題目描述以下:java
編寫一個程序,開啓三個線程,這三個線程的 ID 分別是 A、B 和 C,每一個線程把本身的 ID 在屏幕上打印 10 遍,要求輸出結果必須按 ABC 的順序顯示,如 ABCABCABC... 依次遞推
這是一道經典的多線程編程面試題,首先吐槽一下,這道題的需求非常奇葩,先開啓多線程,而後再串行打印 ABC,這不是吃飽了撐的嗎?不過既然是道面試題,就無論這些了,其目的在於考察你的多線程編程基礎。就這道題,你要是寫不出個三四種解法,你都很差意思說你學過多線程。哈哈開玩笑,下面就爲你介紹一下本題的幾種解法。面試
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
這種方法就是直接使用 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
使用 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中文意思是信號量,本來是操做系統中的概念,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
本文一共介紹了四種三個線程交替打印的實現方法,其中第一種方法最簡單易懂,可是更能考察多線程編程功底的應該是第二和第三種方法,在面試中也更加分。只要把這幾種方法熟練掌握並完全理解,之後碰到此類題型就不用慌了。