面試官:請(qǐng)用五種方法來實(shí)現(xiàn)多線程交替打印問題
三個(gè)線程T1、T2、T3,如何讓他們按順序執(zhí)行?
這是一道面試中常考的并發(fā)編程的代碼題,與它相似的問題有:
三個(gè)線程T1、T2、T3輪流打印ABC,打印n次,如ABCABCABCABC....... 兩個(gè)線程交替打印1-100的奇偶數(shù) N個(gè)線程循環(huán)打印1-100 ......
其實(shí)這類問題本質(zhì)上都是線程通信問題,思路基本上都是一個(gè)線程執(zhí)行完畢,阻塞該線程,喚醒其他線程,按順序執(zhí)行下一個(gè)線程。下面先來看最簡單的,如何按順序執(zhí)行三個(gè)線程。
synchronized+wait/notify
基本思路就是線程A、線程B、線程C三個(gè)線程同時(shí)啟動(dòng),因?yàn)樽兞?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">num的初始值為0,所以線程B或線程C拿到鎖后,進(jìn)入while()循環(huán),然后執(zhí)行wait()方法,線程線程阻塞,釋放鎖。只有線程A拿到鎖后,不進(jìn)入while()循環(huán),執(zhí)行num++,打印字符A,最后喚醒線程B和線程C。此時(shí)num值為1,只有線程B拿到鎖后,不被阻塞,執(zhí)行num++,打印字符B,最后喚醒線程A和線程C,后面以此類推。
class?Wait_Notify_ACB?{
????private?int?num;
????private?static?final?Object?LOCK?=?new?Object();
????private?void?printABC(int?targetNum)?{
????????????synchronized?(LOCK)?{
????????????????while?(num?%?3?!=?targetNum)?{????//想想這里為什么不能用if代替while,想不起來可以看公眾號(hào)上一篇文章
????????????????????try?{
????????????????????????LOCK.wait();
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????e.printStackTrace();
????????????????????}
????????????????}
????????????????num++;
????????????????System.out.print(Thread.currentThread().getName());
????????????????LOCK.notifyAll();
????????????}
????}
????
????public?static?void?main(String[]?args)?{
????????Wait_Notify_ACB??wait_notify_acb?=?new?Wait_Notify_ACB?();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(0);
????????},?"A").start();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(1);
????????},?"B").start();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(2);
????????},?"C").start();
????}
}
輸入結(jié)果:
ABC
Process?finished?with?exit?code?0
接下來看看第一個(gè)問題,三個(gè)線程T1、T2、T3輪流打印ABC,打印n次。其實(shí)只需要將上述代碼加一個(gè)循環(huán)即可,這里假設(shè)n=10。
class?Wait_Notify_ACB?{
????private?int?num;
????private?static?final?Object?LOCK?=?new?Object();
????private?void?printABC(int?targetNum)?{
????????for?(int?i?=?0;?i?10;?i++)?{
????????????synchronized?(LOCK)?{
????????????????while?(num?%?3?!=?targetNum)?{?//想想這里為什么不能用if代替,想不起來可以看公眾號(hào)上一篇文章
????????????????????try?{
????????????????????????LOCK.wait();
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????e.printStackTrace();
????????????????????}
????????????????}
????????????????num++;
????????????????System.out.print(Thread.currentThread().getName());
????????????????LOCK.notifyAll();
????????????}
????????}
????}
????public?static?void?main(String[]?args)?{
????????Wait_Notify_ACB??wait_notify_acb?=?new?Wait_Notify_ACB?();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(0);
????????},?"A").start();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(1);
????????},?"B").start();
????????new?Thread(()?->?{
????????????wait_notify_acb.printABC(2);
????????},?"C").start();
????}????
}
輸出結(jié)果:
ABCABCABCABCABCABCABCABCABCABC
Process?finished?with?exit?code?0
下面看第二個(gè)問題,兩個(gè)線程交替打印1-100的奇偶數(shù),為了減少輸入所占篇幅,這里將100 改成了10。基本思路上面類似,線程odd先拿到鎖——打印數(shù)字——喚醒線程even——阻塞線程odd,以此循環(huán)。
class??Wait_Notify_Odd_Even{
????private?Object?lock?=?new?Object();
????private?volatile?int?count;
????private?void?printOddEven()?{
????????synchronized?(lock)?{
????????????while?(count?10)?{
????????????????try?{
????????????????????System.out.print(?Thread.currentThread().getName()?+?":");
????????????????????System.out.println(++count);
????????????????????lock.notifyAll();
????????????????????lock.wait();
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????}
????????????//防止count=10后,while()循環(huán)不再執(zhí)行,有子線程被阻塞未被喚醒,導(dǎo)致主線程不能退出
????????????lock.notifyAll();
????????}
????}
????
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????Wait_Notify_Odd_Even?waitNotifyOddEven?=?new?Wait_Notify_Odd_Even();
????????new?Thread(waitNotifyOddEven::printOddEven,?"odd").start();
????????Thread.sleep(10);?//為了保證線程odd先拿到鎖
????????new?Thread(waitNotifyOddEven::printOddEven,?"even").start();
????}
}
運(yùn)行結(jié)果:
odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10
再看第三個(gè)問題,N個(gè)線程循環(huán)打印1-100,其實(shí)仔細(xì)想想這個(gè)和三個(gè)線程循環(huán)打印ABC并沒有什么本質(zhì)區(qū)別,只需要加上判斷是否到了打印數(shù)字的最大值的語句即可。假設(shè)N=3,為了能把輸出結(jié)果完全顯示,打印1-10,代碼如下:
class?Wait_Notify_100?{
????private?int?num;
????private?static?final?Object?LOCK?=?new?Object();
????private?int?maxnum?=?10;
????private?void?printABC(int?targetNum)?{
????????while?(true)?{
????????????synchronized?(LOCK)?{
????????????????while?(num?%?3?!=?targetNum)?{?//想想這里為什么不能用if代替,想不起來可以看公眾號(hào)上一篇文章
????????????????????if(num?>=?maxnum){
????????????????????????break;
????????????????????}
????????????????????try?{
????????????????????????LOCK.wait();
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????e.printStackTrace();
????????????????????}
????????????????}
????????????????if(num?>=?maxnum){
????????????????????break;
????????????????}
????????????????num++;
????????????????System.out.println(Thread.currentThread().getName()?+?":?"?+?num);
????????????????LOCK.notifyAll();
????????????}
????????}
????}
????
????public?static?void?main(String[]?args)?{
????????Wait_Notify_100??wait_notify_100?=?new?Wait_Notify_100?();
????????new?Thread(()?->?{
????????????wait_notify_100.printABC(0);
????????},?"thread1").start();
????????new?Thread(()?->?{
????????????wait_notify_100.printABC(1);
????????},?"thread2").start();
????????new?Thread(()?->?{
????????????wait_notify_100.printABC(2);
????????},?"thread3").start();
????}????
}
輸出結(jié)果:
thread1:?1
thread2:?2
thread3:?3
thread1:?4
thread2:?5
thread3:?6
thread1:?7
thread2:?8
thread3:?9
thread1:?10
面試官: 大家都是用的synchronized+wait/notify,你能不能換個(gè)方法解決該問題?
我: 好的,我還會(huì)用join()方法
下面介紹的方法只給出第一道題的代碼了,否則太長了,相信大家可以舉一反三
join()
join()方法:在A線程中調(diào)用了B線程的join()方法時(shí),表示只有當(dāng)B線程執(zhí)行完畢時(shí),A線程才能繼續(xù)執(zhí)行。基于這個(gè)原理,我們使得三個(gè)線程按順序執(zhí)行,然后循環(huán)多次即可。無論線程1、線程2、線程3哪個(gè)先執(zhí)行,最后執(zhí)行的順序都是線程1——>線程2——>線程3。代碼如下:
class?Join_ABC?{
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????for?(int?i?=?0;?i?10;?i++)?{
????????????Thread?t1?=?new?Thread(new?printABC(null),"A");
????????????Thread?t2?=?new?Thread(new?printABC(t1),"B");
????????????Thread?t3?=?new?Thread(new?printABC(t2),"C");
????????????t0.start();
????????????t1.start();
????????????t2.start();
????????????Thread.sleep(10);?//這里是要保證只有t1、t2、t3為一組,進(jìn)行執(zhí)行才能保證t1->t2->t3的執(zhí)行順序。
????????}
????}
????static?class?printABC?implements?Runnable{
????????private?Thread?beforeThread;
????????public?printABC(Thread?beforeThread)?{
????????????this.beforeThread?=?beforeThread;
????????}
????????@Override
????????public?void?run()?{
????????????if(beforeThread!=null)?{
????????????????try?{
????????????????????beforeThread.join();?
????????????????????System.out.print(Thread.currentThread().getName());
????????????????}catch(Exception?e){
????????????????????e.printStackTrace();
????????????????}
????????????}else?{
????????????????System.out.print(Thread.currentThread().getName());
????????????}
????????}
????}
}
輸出結(jié)果:
ABCABCABCABCABCABCABCABCABCABC
面試官: 還會(huì)其他方法嗎?
我: 還會(huì)使用Lock解決該問題。
Lock
該方法很容易理解,不管哪個(gè)線程拿到鎖,只有符合條件的才能打印。代碼如下:
?class?Lock_ABC?{
????private?int?num;???//?當(dāng)前狀態(tài)值:保證三個(gè)線程之間交替打印
????private?Lock?lock?=?new?ReentrantLock();
????private?void?printABC(int?targetNum)?{
????????for?(int?i?=?0;?i?10;?)?{
????????????lock.lock();
????????????if?(num?%?3?==?targetNum)?{
????????????????num++;
????????????????i++;
????????????????System.out.print(Thread.currentThread().getName());
????????????}
????????????lock.unlock();
????????}
????}
????public?static?void?main(String[]?args)?{
????????Lock_ABC?lockABC?=?new?Lock_ABC();
????????new?Thread(()?->?{
????????????lockABC.printABC(0);
????????},?"A").start();
????????new?Thread(()?->?{
????????????lockABC.printABC(1);
????????},?"B").start();
????????new?Thread(()?->?{
????????????lockABC.printABC(2);
????????},?"C").start();
????}
}
輸出結(jié)果:
ABCABCABCABCABCABCABCABCABCABC
面試官: 該方法存在什么問題,可以進(jìn)一步優(yōu)化嗎
我: 可以使用Lock+Condition實(shí)現(xiàn)對(duì)線程的精準(zhǔn)喚醒,減少對(duì)同步鎖的無意義競爭,浪費(fèi)資源。
Lock+Condition
該思路和synchronized+wait/notify方法的很像,synchronized對(duì)應(yīng)lock,await/signal方法對(duì)應(yīng)wait/notify方法。下面的代碼為了能精準(zhǔn)地喚醒下一個(gè)線程,創(chuàng)建了多個(gè)Condition對(duì)象。
class?LockConditionABC?{
????private?int?num;
????private?static?Lock?lock?=?new?ReentrantLock();
????private?static?Condition?c1?=?lock.newCondition();
????private?static?Condition?c2?=?lock.newCondition();
????private?static?Condition?c3?=?lock.newCondition();
????private?void?printABC(int?targetNum,?Condition?currentThread,?Condition?nextThread)?{
????????for?(int?i?=?0;?i?10;?)?{
????????????lock.lock();
????????????try?{
????????????????while?(num?%?3?!=?targetNum)?{
????????????????????currentThread.await();??//阻塞當(dāng)前線程
????????????????}
????????????????num++;
????????????????i++;
????????????????System.out.print(Thread.currentThread().getName());
????????????????nextThread.signal();????//喚醒下一個(gè)線程,而不是喚醒所有線程
????????????}?catch?(Exception?e)?{
????????????????e.printStackTrace();
????????????}?finally?{
????????????????lock.unlock();
????????????}
????????}
????}
????public?static?void?main(String[]?args)?{
????????LockConditionABC?print?=?new?LockConditionABC();
????????new?Thread(()?->?{
????????????print.printABC(0,?c1,?c2);
????????},?"A").start();
????????new?Thread(()?->?{
????????????print.printABC(1,?c2,?c3);
????????},?"B").start();
????????new?Thread(()?->?{
????????????print.printABC(2,?c3,?c1);
????????},?"C").start();
????}
}
輸出結(jié)果:
ABCABCABCABCABCABCABCABCABCABC
面試官: 除了該方法,還有什么方法可以避免喚醒其他無意義的線程避免資源浪費(fèi)?
我: 可以通過使用信號(hào)量來實(shí)現(xiàn)。
Semaphore
Semaphore:用來控制同時(shí)訪問某個(gè)特定資源的操作數(shù)量,或者同時(shí)執(zhí)行某個(gè)指定操作的數(shù)量。Semaphore內(nèi)部維護(hù)了一個(gè)計(jì)數(shù)器,其值為可以訪問的共享資源的個(gè)數(shù)。
一個(gè)線程要訪問共享資源,先使用acquire()方法獲得信號(hào)量,如果信號(hào)量的計(jì)數(shù)器值大于等于1,意味著有共享資源可以訪問,則使其計(jì)數(shù)器值減去1,再訪問共享資源。如果計(jì)數(shù)器值為0,線程進(jìn)入休眠。
當(dāng)某個(gè)線程使用完共享資源后,使用release()釋放信號(hào)量,并將信號(hào)量內(nèi)部的計(jì)數(shù)器加1,之前進(jìn)入休眠的線程將被喚醒并再次試圖獲得信號(hào)量。
代碼如下:
class?SemaphoreABC?{
????private?static?Semaphore?s1?=?new?Semaphore(1);?//因?yàn)橄葓?zhí)行線程A,所以這里設(shè)s1的計(jì)數(shù)器為1
????private?static?Semaphore?s2?=?new?Semaphore(0);
????private?static?Semaphore?s3?=?new?Semaphore(0);
????private?void?printABC(Semaphore?currentThread,?Semaphore?nextThread)?{
????????for?(int?i?=?0;?i?10;?i++)?{
????????????try?{
????????????????currentThread.acquire();???????//阻塞當(dāng)前線程,即信號(hào)量的計(jì)數(shù)器減1為0
????????????????System.out.print(Thread.currentThread().getName());
????????????????nextThread.release();??????????//喚醒下一個(gè)線程,即信號(hào)量的計(jì)數(shù)器加1
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????SemaphoreABC?printer?=?new?SemaphoreABC();
????????new?Thread(()?->?{
????????????printer.printABC(s1,?s2);
????????},?"A").start();
????????Thread.sleep(10);
????????new?Thread(()?->?{
????????????printer.printABC(s2,?s3);
????????},?"B").start();
????????Thread.sleep(10);
????????new?Thread(()?->?{
????????????printer.printABC(s3,?s1);
????????},?"C").start();
????}
}
輸出結(jié)果:
ABCABCABCABCABCABCABCABCABCABC
面試官: 除了上述五種方法,還有其他方法解決多線程交替打印問題嗎
我: 還有LockSupport、CountDownLatch、AtomicInteger等等。
面試官: 那如何實(shí)現(xiàn)三個(gè)線程循環(huán)打印ACB,其中A打印兩次,B打印三次,C打印四次呢?
我:......
面試官:如何用兩個(gè)線程交叉打印數(shù)字和字符呢?例如A1B2C3......Z26
我:......
真實(shí)的面試中,面試官肯定不會(huì)讓用這么多方法解決多線程交替打印問題,大家記住一兩種就可以了,上面中的面試場景純屬瞎編。大家可以思考下后面兩個(gè)升級(jí)版的問題,原理都是相通的。
