遇到個(gè)面試題,挺有意思
大家好,我是魚(yú)皮,最近看到一個(gè)面試題目,感覺(jué)挺有意思的,大意如下:

ok,大家看到這個(gè)題,可以先理解下,這里啟動(dòng)了兩個(gè)線(xiàn)程,a 和 b,但是雖然說(shuō) a 在 b 之前 start,不一定就可以保證線(xiàn)程 a 的邏輯,可以先于線(xiàn)程 b 執(zhí)行。
所以,這里的意思是,線(xiàn)程 a 和 b,執(zhí)行順序互不干擾,我們不應(yīng)該假定其中一個(gè)線(xiàn)程可以先于另外一個(gè)執(zhí)行。
另外,既然是面試題,那常規(guī)做法自然是不用上了,比如讓 b 先 sleep 幾秒鐘之類(lèi)的,如果真這么答,那可能面試就結(jié)束了吧。
好,我們下面開(kāi)始分析解法。
可見(jiàn)性保證
程序里定義了一個(gè)全局變量,var = 1。
線(xiàn)程a會(huì)修改這個(gè)變量為2,線(xiàn)程b則在變量為2時(shí),執(zhí)行自己的業(yè)務(wù)邏輯。
那么,這里首先,我們要做的是,先講var使用volatile修飾,保證多線(xiàn)程操作時(shí)的可見(jiàn)性。
public static volatile int var = 1;
解法分析
經(jīng)過(guò)前面的可見(jiàn)性保證的分析,我們知道,要想達(dá)到目的,其實(shí)就是要保證:
a中的對(duì)var+1的操作,需要先于b執(zhí)行。
但是,現(xiàn)在的問(wèn)題是,兩個(gè)線(xiàn)程同時(shí)啟動(dòng),不知道誰(shuí)先誰(shuí)后,怎么保證 a 先執(zhí)行,b 后執(zhí)行呢?
讓線(xiàn)程 b 先不執(zhí)行,大概有兩種思路:一種是阻塞該線(xiàn)程,一種是不阻塞該線(xiàn)程。阻塞的話(huà),我們可以想想,怎么阻塞一個(gè)線(xiàn)程。
大概有下面這些方法:
synchronized,取不到鎖時(shí),阻塞 java.util.concurrent.locks.ReentrantLock#lock,取不到鎖時(shí),阻塞 object.wait,取到synchronized了,但是因?yàn)橐恍l件不滿(mǎn)足,執(zhí)行不下去,調(diào)用wait,將釋放鎖,并進(jìn)入等待隊(duì)列,線(xiàn)程暫停運(yùn)行 java.util.concurrent.locks.Condition.await,和object.wait類(lèi)似,只不過(guò)object.wait在jvm層面,使用c++實(shí)現(xiàn),Condition.await在jdk層面使用java語(yǔ)言實(shí)現(xiàn) threadA.join(),等待對(duì)應(yīng)的線(xiàn)程threadA執(zhí)行完成后,本線(xiàn)程再繼續(xù)運(yùn)行;threadA沒(méi)結(jié)束,則當(dāng)前線(xiàn)程阻塞; CountDownLatch#await,在對(duì)應(yīng)的state不為0時(shí),阻塞 Semaphore#acquire(),在state為0時(shí)(即剩余令牌為0時(shí)),阻塞 其他阻塞隊(duì)列、FutureTask等等
如果不讓線(xiàn)程進(jìn)入阻塞,則一般可以讓線(xiàn)程進(jìn)入一個(gè)while循環(huán),循環(huán)的退出條件,可以由線(xiàn)程a來(lái)修改,線(xiàn)程a修改后,線(xiàn)程b跳出循環(huán)。
比如:
volatile boolean stop = false;
while (!stop){
...
}
上面也說(shuō)了這么多了,我們實(shí)際上手寫(xiě)一寫(xiě)吧。
錯(cuò)誤解法1--基于wait
下面的思路是基于wait、notify。
線(xiàn)程b直接wait,線(xiàn)程a在修改了變量后,進(jìn)行notify。
public class Global1 {
public static volatile int var = 1;
public static final Object monitor = new Object();
public static void main(String[] args) {
Thread a = new Thread(() -> {
// 1
Global1.var++;
// 2
synchronized (monitor) {
monitor.notify();
}
});
Thread b = new Thread(() -> {
// 3
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 4
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
a.start();
b.start();
}
}
大家覺(jué)得這個(gè)代碼能行嗎?
實(shí)際是不行的。因?yàn)閷?shí)際的順序可能是:
線(xiàn)程a--1
線(xiàn)程a--2
線(xiàn)程b--1
線(xiàn)程b--2
在線(xiàn)程 a-2 時(shí),線(xiàn)程 a 去 notify,但是此時(shí)線(xiàn)程 b 還沒(méi)開(kāi)始 wait,所以此時(shí)的 notify 是沒(méi)有任何效果的:
沒(méi)人在等,notify 個(gè)錘子。
怎么修改,本方案才行得通呢?
那就是,修改線(xiàn)程 a 的代碼,不要急著 notify,先等等。
Thread a = new Thread(() -> {
Global1.var++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (monitor) {
monitor.notify();
}
});
但是這樣的話(huà),明顯不合適,有作弊嫌疑,也不優(yōu)雅。
錯(cuò)誤解法2--基于condition的signal
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Global1 {
public static volatile int var = 1;
public static final ReentrantLock reentrantLock = new ReentrantLock();
public static final Condition condition = reentrantLock.newCondition();
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
final ReentrantLock lock = reentrantLock;
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
});
Thread b = new Thread(() -> {
final ReentrantLock lock = reentrantLock;
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
a.start();
b.start();
}
}
這個(gè)方案使用了 Condition 對(duì)象來(lái)實(shí)現(xiàn) object 的 notify、wait 效果。當(dāng)然,這個(gè)也有同樣的問(wèn)題。
正確解法1--基于錯(cuò)誤解法2進(jìn)行改進(jìn)
我們看看,前面問(wèn)題的根源在于,我們線(xiàn)程 a,在去通知線(xiàn)程 b 的時(shí)候,有可能線(xiàn)程 b 還沒(méi)開(kāi)始 wait,所以此時(shí)通知失效。
那么,我們是不是可以先等等,等線(xiàn)程 b 開(kāi)始 wait 了,再去通知呢?
Thread a = new Thread(() -> {
Global1.var++;
final ReentrantLock lock = reentrantLock;
lock.lock();
try {
// 1
while (!reentrantLock.hasWaiters(condition)) {
Thread.yield();
}
condition.signal();
} finally {
lock.unlock();
}
});
1 處代碼,就是這個(gè)思想,在 signal 之前,判斷當(dāng)前 condition 上是否有 waiter 線(xiàn)程,如果沒(méi)有,就死循環(huán);如果有,才去執(zhí)行 signal。
這個(gè)方法實(shí)測(cè)是可行的。
正確解法2
對(duì)正確解法 1,換一個(gè) api,就變成了正確解法 2.
Thread a = new Thread(() -> {
Global1.var++;
final ReentrantLock lock = reentrantLock;
lock.lock();
try {
// 1
while (reentrantLock.getWaitQueueLength(condition) == 0) {
Thread.yield();
}
condition.signal();
} finally {
lock.unlock();
}
});
1 這里,獲取 condition 上等待隊(duì)列的長(zhǎng)度,如果為 0,說(shuō)明沒(méi)有等待者,則死循環(huán)。
正確解法3--基于Semaphore
剛開(kāi)始,我們初始化一個(gè)信號(hào)量,state 為 0。
線(xiàn)程 b 去獲取信號(hào)量的時(shí)候,就會(huì)阻塞。
然后我們線(xiàn)程 a 再去釋放一個(gè)信號(hào)量,此時(shí)線(xiàn)程 b 就可以繼續(xù)執(zhí)行。
public class Global1 {
public static volatile int var = 1;
public static final Semaphore semaphore = new Semaphore(0);
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
semaphore.release();
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
正確解法4--基于CountDownLatch
public class Global1 {
public static volatile int var = 1;
public static final CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
countDownLatch.countDown();
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
正確解法5--基于BlockingQueue#
這里使用了 ArrayBlockingQueue,其他的阻塞隊(duì)列也是可以的。
public class Global1 {
public static volatile int var = 1;
public static final ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<Object>(1);
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
arrayBlockingQueue.offer(new Object());
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
arrayBlockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
正確解法6--基于FutureTask
我們也可以讓線(xiàn)程 b 等待一個(gè) task 的執(zhí)行結(jié)果。
而線(xiàn)程 a 在執(zhí)行完修改 var 為 2 后,執(zhí)行該任務(wù),任務(wù)執(zhí)行完成后,線(xiàn)程 b 就會(huì)被通知繼續(xù)執(zhí)行。
public class Global1 {
public static volatile int var = 1;
public static final FutureTask futureTask = new FutureTask<Object>(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println("callable task ");
return null;
}
});
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
futureTask.run();
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
正確解法7--基于join
這個(gè)可能是最簡(jiǎn)潔直觀的解法:
public class Global1 {
public static volatile int var = 1;
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
正確解法8--基于CompletableFuture
這個(gè)和第 6 種類(lèi)似。都是基于 future。
public class Global1 {
public static volatile int var = 1;
public static final CompletableFuture<Object> completableFuture =
new CompletableFuture<Object>();
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
completableFuture.complete(new Object());
});
a.setName("thread a");
Thread b = new Thread(() -> {
try {
completableFuture.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
非阻塞--正確解法9--忙等待
這種代碼量也少,只要線(xiàn)程 b 在變量為 1 時(shí),死循環(huán)就行了。
public class Global1 {
public static volatile int var = 1;
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
});
a.setName("thread a");
Thread b = new Thread(() -> {
while (var == 1) {
Thread.yield();
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
非阻塞--正確解法10--忙等待
忙等待的方案很多,反正就是某個(gè)條件不滿(mǎn)足時(shí),不阻塞自己,阻塞了會(huì)釋放 cpu,我們就是不希望釋放 cpu 的。
比如像下面這樣也可以:
public class Global1 {
public static volatile int var = 1;
public static final AtomicInteger atomicInteger =
new AtomicInteger(1);
public static void main(String[] args) {
Thread a = new Thread(() -> {
Global1.var++;
atomicInteger.set(2);
});
a.setName("thread a");
Thread b = new Thread(() -> {
while (true) {
boolean success = atomicInteger.compareAndSet(2, 1);
if (success) {
break;
} else {
Thread.yield();
}
}
if (Global1.var == 2) {
//do something;
System.out.println(Thread.currentThread().getName() + " good job");
}
});
b.setName("thread b");
a.start();
b.start();
}
}
暫時(shí)想了這么些,方案還是比較多的,大家可以開(kāi)動(dòng)腦筋,頭腦風(fēng)暴吧。
看看你還有什么騷操作,可以在評(píng)論區(qū)留言。
以上就是本期分享了。
最后,歡迎加入 魚(yú)皮的編程知識(shí)星球(點(diǎn)擊了解詳情),和大家一起交流學(xué)習(xí)編程,向魚(yú)皮和大廠(chǎng)同學(xué) 1 對(duì) 1 提問(wèn)、幫你制定學(xué)習(xí)計(jì)劃不迷茫、跟著魚(yú)皮直播做項(xiàng)目(往期項(xiàng)目可無(wú)限回看)、領(lǐng)取魚(yú)皮原創(chuàng)編程學(xué)習(xí)/求職資料等。最近秋招開(kāi)始了,星球內(nèi)也會(huì)幫大家規(guī)劃求職進(jìn)度、完善簡(jiǎn)歷和項(xiàng)目。

往期推薦
