題目描述如下:
編寫一個程序,開啟三個線程,這三個線程的 ID 分別是 A验游、B 和 C充岛,每個線程把自己的 ID 在屏幕上打印 10 遍,要求輸出結(jié)果必須按 ABC 的順序顯示耕蝉,如 ABCABCABC... 依次遞推
這是一道經(jīng)典的多線程編程面試題崔梗,首先吐槽一下,這道題的需求很是奇葩垒在,先開啟多線程蒜魄,然后再串行打印 ABC,這不是吃飽了撐的嗎场躯?不過既然是道面試題谈为,就不管這些了,其目的在于考察你的多線程編程基礎推盛。就這道題峦阁,你要是寫不出個三四種解法,你都不好意思說你學過多線程耘成。哈哈開玩笑,下面就為你介紹一下本題的幾種解法驹闰。
1瘪菌、最簡單的方法——使用 LockSupport
LockSupport 是java.util.concurrent.locks
包下的工具類,它的靜態(tài)方法unpark()
和park()
可以分別實現(xiàn)阻塞當前線程和喚醒指定線程的效果嘹朗,所以用它解決這樣的問題簡直是小菜一碟师妙,代碼如下:
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();
}
}
執(zhí)行結(jié)果如下:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
2、最傳統(tǒng)的方法——使用synchronized 鎖機制
這種方法就是直接使用 Java 的 synchronized 關鍵字屹培,配合 Object 的 wait()
和notifyAll()
方法實現(xiàn)線程交替打印的效果默穴,不過這種寫法的復雜度和代碼量都偏大。由于notify()
和notifyAll()
方法都不能喚醒指定的線程褪秀,所以需要三個布爾變量對線程執(zhí)行順序進行控制蓄诽。另外要注意的就是,for 循環(huán)中的 i++
需要在線程打印之后執(zhí)行媒吗,否則每次被喚醒后仑氛,不管是不是輪到當前線程打印都會執(zhí)行i++
,這顯然不是我們想要的闸英。代碼如下 (一般B锯岖、C線程和A線程的執(zhí)行邏輯類似,只在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 {
// 說明沒有輪到當前線程打印辙喂,繼續(xù)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();
}
}
執(zhí)行結(jié)果如下:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
3捶牢、使用 Lock 搭配 Condition 實現(xiàn)
使用 synchronized 鎖機制的寫法著實有些復雜赃额,何不試試 ReentrantLock?這是java.util.concurrent.locks
包下的鎖實現(xiàn)類叫确,它擁有更靈活的 API跳芳,能夠?qū)Χ嗑€程執(zhí)行流程實現(xiàn)更精細的控制,特別是在搭配 Condition 使用的情況下竹勉,可以隨心所欲地控制多個線程的執(zhí)行順序飞盆,來看看這個組合在本題中的使用吧,代碼如下:
public class PrintABC {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 使用ReentrantLock的newCondition()方法創(chuàng)建三個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();
}
// 這里有個坑,要記得在循環(huán)之后調(diào)用signal()票腰,否則線程可能會一直處于
// wait狀態(tài)城看,導致程序無法結(jié)束
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 在finally代碼塊調(diào)用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();
}
}
執(zhí)行結(jié)果如下:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
4、使用 Semaphore 實現(xiàn)
semaphore中文意思是信號量杏慰,原本是操作系統(tǒng)中的概念测柠,JUC下也有個 Semaphore 的類,可用于控制并發(fā)線程的數(shù)量缘滥。Semaphore 的構造方法有個 int 類型的 permits 參數(shù)轰胁,如下:
public Semaphore(int permits) {...}
其中 permits 指的是該 Semaphore 對象可分配的許可數(shù),一個線程中的 Semaphore 對象調(diào)用acquire()
方法可以讓線程獲取許可繼續(xù)運行朝扼,同時該對象的許可數(shù)減一赃阀,如果當前沒有可用許可,線程會阻塞擎颖。該 Semaphore 對象調(diào)用release()
方法可以釋放許可榛斯,同時其許可數(shù)加一。Talk is cheap, show me the code!
public class PrintABC {
public static void main(String[] args) {
// 初始化許可數(shù)為1搂捧,A線程可以先執(zhí)行
Semaphore semaphoreA = new Semaphore(1);
// 初始化許可數(shù)為0驮俗,B線程阻塞
Semaphore semaphoreB = new Semaphore(0);
// 初始化許可數(shù)為0,C線程阻塞
Semaphore semaphoreC = new Semaphore(0);
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
// A線程獲得許可异旧,同時semaphoreA的許可數(shù)減為0,進入下一次循環(huán)時
// A線程會阻塞意述,知道其他線程執(zhí)行semaphoreA.release();
semaphoreA.acquire();
// 打印當前線程名稱
System.out.print(Thread.currentThread().getName());
// semaphoreB許可數(shù)加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();
}
}
執(zhí)行結(jié)果如下:
ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0
5、總結(jié)
本文一共介紹了四種三個線程交替打印的實現(xiàn)方法吮蛹,其中第一種方法最簡單易懂荤崇,但是更能考察多線程編程功底的應該是第二和第三種方法,在面試中也更加分潮针。只要把這幾種方法熟練掌握并徹底理解术荤,以后碰到此類題型就不用慌了。