1. 程式人生 > >曹工說面試題:一個執行緒協同問題,解法繁多,都要被玩壞了,趁著沒壞,一起玩吧

曹工說面試題:一個執行緒協同問題,解法繁多,都要被玩壞了,趁著沒壞,一起玩吧

# 前言 最近兩個月寫文章很少,因為自己學習狀態也不是很好,我看了下,上一篇文章,都是一個月前了。 不知道大家有沒有感覺,小學初中讀的一些書,看的一些文章,到現在都印象深刻,反倒是高中學的知識,高考後就慢慢消散,直到遺忘。 我想說的是,記得初中學過魯迅的《藤野先生》,裡面有一段話,大意是:久了不聯絡,有時候想聯絡,卻又無從下筆,到最後就更是不了了之了。 我找了下原文: >將走的前幾天,他叫我到他家裡去,交給我一張照相,後面寫著兩個字道:“惜別”,還說希望將我的也送他。但我這時適值沒有照相了;他便叮囑我將來照了寄給他,並且時時通訊告訴他此後的狀況。 > >我離開仙台之後,就多年沒有照過相,又因為狀況也無聊,說起來無非使他失望,便連信也怕敢寫了。經過的年月一多,話更無從說起,所以雖然有時想寫信,卻又難以下筆,這樣的一直到現在,竟沒有寄過一封信和一張照片。從他那一面看起來,是一去之後,杳無訊息了。 其實寫文章也是這樣的,久了不寫更不想寫,但是心裡又時時記著這麼個事情,玩也不是很自在;今天先隨便寫一下,找下狀態吧,因為現在文章可能在部落格和公眾號發,比如部落格,一般來說會隨意點,但是公眾號的話,一般大家質量要求會高一些,結果就是,為了追求高質量,而非要找到一些很厲害的技術點,或者自己研究透了才動筆,這樣會導致一些想法難產,因為可能覺得很簡單,不值得發到公眾號,實際上,很多時候都是浮於表面地覺得很簡單,一旦深挖,立馬就廢。 扯這麼多,也是給我自己,或者其他剛開始寫技術公眾號的同學,也不用覺得非要寫的多麼多麼好才發出來,本來大家都是一步一步來的,各種大佬也不是一下就變成大佬的,把自己的學習過程和成長過程發出來,大家也就知道:哦,大佬原來也這麼菜啊,哈哈。 比如最近看到一些演算法大佬,一開始也是10道演算法題,全部都要看答案的好麼。。 扯了不少,言歸正傳吧,最近在網上看到一個面試題目,感覺挺有意思的,大意如下: ![](https://img2020.cnblogs.com/blog/519126/202009/519126-20200926203742890-403133210.png) ok,大家看到這個題,可以先理解下,這裡啟動了兩個執行緒,a和b,但是雖然說a在b之前start,不一定就可以保證執行緒a的邏輯,可以先於執行緒b執行,所以,這裡的意思是,執行緒a和b,執行順序互不干擾,我們不應該假定其中一個執行緒可以先於另外一個執行。 另外,既然是面試題,那常規做法自然是不用上了,比如讓b先sleep幾秒鐘之類的,如果真這麼答,那可能面試就結束了吧。 ok,我們下面開始分析解法。 # 可見性保證 程式裡定義了一個全域性變數,var = 1;執行緒a會修改這個變數為2,執行緒b則在變數為2時,執行自己的業務邏輯。 那麼,這裡首先,我們要做的是,先講var使用volatile修飾,保證多執行緒操作時的可見性。 ```java public static volatile int var = 1; ``` # 解法分析 經過前面的可見性保證的分析,我們知道,要想達到目的,其實就是要保證: **a中的對var+1的操作,需要先於b執行。** 但是,現在的問題是,兩個執行緒同時啟動,不知道誰先誰後,怎麼保證a先執行,b後執行呢? 讓執行緒b先不執行,大概有兩種思路,一種是阻塞該執行緒,一種是不阻塞該執行緒,阻塞的話,我們可以想想,怎麼阻塞一個執行緒。 大概有: * synchronized,取不到鎖時,阻塞 * java.util.concurrent.locks.ReentrantLock#lock,取不到鎖時,阻塞 * object.wait,取到synchronized了,但是因為一些條件不滿足,執行不下去,呼叫wait,將釋放鎖,並進入等待佇列,執行緒暫停執行 * java.util.concurrent.locks.Condition.await,和object.wait類似,只不過object.wait在jvm層面,使用c++實現,Condition.await在jdk層面使用java語言實現 * threadA.join(),等待對應的執行緒threadA執行完成後,本執行緒再繼續執行;threadA沒結束,則當前執行緒阻塞; * CountDownLatch#await,在對應的state不為0時,阻塞 * Semaphore#acquire(),在state為0時(即剩餘令牌為0時),阻塞 * 其他阻塞佇列、FutureTask等等 如果不讓執行緒進入阻塞,則一般可以讓執行緒進入一個while迴圈,迴圈的退出條件,可以由執行緒a來修改,執行緒a修改後,執行緒b跳出迴圈。 比如: ```java volatile boolean stop = false; while (!stop){ ... } ``` 上面也說了這麼多了,我們實際上手寫一寫吧。 # 錯誤解法1--基於wait 下面的思路是基於wait、notify;執行緒b直接wait,執行緒a在修改了變數後,進行notify。 ```java 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(); } } ``` 大家覺得這個程式碼能行嗎?實際是不行的。因為實際的順序可能是: ``` 執行緒a--1 執行緒a--2 執行緒b--1 執行緒b--2 ``` 線上程a-2時,執行緒a去notify,但是此時執行緒b還沒開始wait,所以此時的notify是沒有任何效果的:沒人在等,notify個錘子。 怎麼修改,本方案才行得通呢? 那就是,修改執行緒a的程式碼,不要急著notify,先等等。 ```java Thread a = new Thread(() -> { Global1.var++; try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (monitor) { monitor.notify(); } }); ``` 但是這樣的話,明顯不合適,有作弊嫌疑,也不優雅。 # 錯誤解法2--基於condition的signal ```java 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(); } } ``` 這個方案使用了Condition物件來實現object的notify、wait效果。當然,這個也有同樣的問題。 # 正確解法1--基於錯誤解法2進行改進 我們看看,前面問題的根源在於,我們執行緒a,在去通知執行緒b的時候,有可能執行緒b還沒開始wait,所以此時通知失效。 那麼,我們是不是可以先等等,等執行緒b開始wait了,再去通知呢? ```java 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處程式碼,就是這個思想,在signal之前,判斷當前condition上是否有waiter執行緒,如果沒有,就死迴圈;如果有,才去執行signal。 這個方法實測是可行的。 # 正確解法2 對正確解法1,換一個api,就變成了正確解法2. ```java 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上等待佇列的長度,如果為0,說明沒有等待者,則死迴圈。 # 正確解法3--基於Semaphore 剛開始,我們初始化一個訊號量,state為0. 執行緒b去獲取訊號量的時候,就會阻塞。 然後我們執行緒a再去釋放一個訊號量,此時執行緒b就可以繼續執行。 ```java 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 ```java 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,其他的阻塞佇列也是可以的。 ```java import countdown.CountdownTest; public class Global1 { public static volatile int var = 1; public static final ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(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等待一個task的執行結果;而執行緒a在執行完修改var為2後,執行該任務,任務執行完成後,執行緒b就會被通知繼續執行。 ```java public class Global1 { public static volatile int var = 1; public static final FutureTask futureTask = new FutureTask(new Callable() { @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 這個可能是最簡潔直觀的,哈哈。也是群裡同學們提供的解法,真的有才! ```java 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 這個和第6種類似。都是基於future。 ```java public class Global1 { public static volatile int var = 1; public static final CompletableFuture completableFuture = new CompletableFuture(); 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時,死迴圈就行了。 ```java 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--忙等待 忙等待的方案很多,反正就是某個條件不滿足時,不阻塞自己,阻塞了會釋放cpu,我們就是不希望釋放cpu的。 比如像下面這樣也可以。 ```java 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(); } } ``` # 小結 暫時想了這麼寫,方案還是比較多的,大家可以開動腦筋,頭腦風暴吧!我是逐日,混跡成都的老java程式猿,部落格裡有我更多的一些文章,大家可以看看,暫時沒有遷移到公眾號的打算。