1. 面試官:請用五種方法來實(shí)現(xiàn)多線程交替打印問題

        共 7026字,需瀏覽 15分鐘

         ·

        2021-12-13 02:08

        三個(gè)線程T1、T2、T3,如何讓他們按順序執(zhí)行?

        這是一道面試中??嫉牟l(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í)啟動,因?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,想不起來可以看公眾號上一篇文章
        ????????????????????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代替,想不起來可以看公眾號上一篇文章
        ????????????????????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代替,想不起來可以看公眾號上一篇文章
        ????????????????????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è)方法解決該問題?

        我: 好的,我還會用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


        面試官: 還會其他方法嗎?

        我: 還會使用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)對線程的精準(zhǔn)喚醒,減少對同步鎖的無意義競爭,浪費(fèi)資源。

        Lock+Condition

        該思路和synchronized+wait/notify方法的很像,synchronized對應(yīng)lockawait/signal方法對應(yīng)wait/notify方法。下面的代碼為了能精準(zhǔn)地喚醒下一個(gè)線程,創(chuàng)建了多個(gè)Condition對象。

        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)?

        我: 可以通過使用信號量來實(shí)現(xiàn)。

        Semaphore

        Semaphore:用來控制同時(shí)訪問某個(gè)特定資源的操作數(shù)量,或者同時(shí)執(zhí)行某個(gè)指定操作的數(shù)量。Semaphore內(nèi)部維護(hù)了一個(gè)計(jì)數(shù)器,其值為可以訪問的共享資源的個(gè)數(shù)。

        一個(gè)線程要訪問共享資源,先使用acquire()方法獲得信號量,如果信號量的計(jì)數(shù)器值大于等于1,意味著有共享資源可以訪問,則使其計(jì)數(shù)器值減去1,再訪問共享資源。如果計(jì)數(shù)器值為0,線程進(jìn)入休眠。

        當(dāng)某個(gè)線程使用完共享資源后,使用release()釋放信號量,并將信號量內(nèi)部的計(jì)數(shù)器加1,之前進(jìn)入休眠的線程將被喚醒并再次試圖獲得信號量。

        代碼如下:

        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)前線程,即信號量的計(jì)數(shù)器減1為0
        ????????????????System.out.print(Thread.currentThread().getName());
        ????????????????nextThread.release();??????????//喚醒下一個(gè)線程,即信號量的計(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í)的面試中,面試官肯定不會讓用這么多方法解決多線程交替打印問題,大家記住一兩種就可以了,上面中的面試場景純屬瞎編。大家可以思考下后面兩個(gè)升級版的問題,原理都是相通的。

        瀏覽 112
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 裸体男人给裸体男人按摩 | 色涩网站 | 草逼日本 | 男人色色网 | 女生坐男生腿上睾丸疼正常吗 |