背景
在前幾天,群裡有個群友問了我一道面試阿里的時候遇到的多執行緒題目,這個題目比較有意思,在這裡和大家分享一下。
廢話不多說,直接上題目:
通過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();
}
}
複製程式碼
通過這種方式,我們就不會有白白喚醒的執行緒,每一個執行緒都按照我們所約定的順序去執行,這其實也是面試官所需要考的地方,讓每個執行緒的執行都能再你手中得到控制,這也可以驗證你多執行緒知識是否牢固。
感興趣的可以自己來我的Java架構群,可以獲取免費的學習資料,群號:855801563對Java技術,架構技術感興趣的同學,歡迎加群,一起學習,相互討論。