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

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

往期推薦
