華為和阿里都考過的多線程編程題反浓,你會嗎?多線程交替打印 ABC的多種實現(xiàn)方法

題目描述如下:

編寫一個程序,開啟三個線程,這三個線程的 ID 分別是 A验游、B 和 C充岛,每個線程把自己的 ID 在屏幕上打印 10 遍,要求輸出結(jié)果必須按 ABC 的順序顯示耕蝉,如 ABCABCABC... 依次遞推

這是一道經(jīng)典的多線程編程面試題崔梗,首先吐槽一下,這道題的需求很是奇葩垒在,先開啟多線程蒜魄,然后再串行打印 ABC,這不是吃飽了撐的嗎场躯?不過既然是道面試題谈为,就不管這些了,其目的在于考察你的多線程編程基礎推盛。就這道題峦阁,你要是寫不出個三四種解法,你都不好意思說你學過多線程耘成。哈哈開玩笑,下面就為你介紹一下本題的幾種解法驹闰。

1瘪菌、最簡單的方法——使用 LockSupport

LockSupport 是java.util.concurrent.locks包下的工具類,它的靜態(tài)方法unpark()park()可以分別實現(xiàn)阻塞當前線程和喚醒指定線程的效果嘹朗,所以用它解決這樣的問題簡直是小菜一碟师妙,代碼如下:

public class PrintABC {
  
    static Thread threadA, threadB, threadC;
  
    public static void main(String[] args) {
        threadA = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 打印當前線程名稱
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadB);
                // 當前線程阻塞
                LockSupport.park();
            }
        }, "A");
        threadB = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 先阻塞等待被喚醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadC);
            }
        }, "B");
        threadC = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                // 先阻塞等待被喚醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 喚醒下一個線程
                LockSupport.unpark(threadA);
            }
        }, "C");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

執(zhí)行結(jié)果如下:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

2、最傳統(tǒng)的方法——使用synchronized 鎖機制

這種方法就是直接使用 Java 的 synchronized 關鍵字屹培,配合 Object 的 wait()notifyAll()方法實現(xiàn)線程交替打印的效果默穴,不過這種寫法的復雜度和代碼量都偏大。由于notify()notifyAll()方法都不能喚醒指定的線程褪秀,所以需要三個布爾變量對線程執(zhí)行順序進行控制蓄诽。另外要注意的就是,for 循環(huán)中的 i++需要在線程打印之后執(zhí)行媒吗,否則每次被喚醒后仑氛,不管是不是輪到當前線程打印都會執(zhí)行i++,這顯然不是我們想要的闸英。代碼如下 (一般B锯岖、C線程和A線程的執(zhí)行邏輯類似,只在A線程代碼中進行詳細注釋說明):

public class PrintABC {
    // 使用布爾變量對打印順序進行控制甫何,true表示輪到當前線程打印
    private static boolean startA = true;
    private static boolean startB = false;
    private static boolean startC = false;

    public static void main(String[] args) {
        // 作為鎖對象
        final Object o = new Object();
        // A線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startA) {
                        // 代表輪到當前線程打印
                        System.out.print(Thread.currentThread().getName());
                        // 下一個輪到B打印出吹,所以把startB置為true,其它為false
                        startA = false;
                        startB = true;
                        startC = false;
                        // 喚醒其他線程
                        o.notifyAll();
                        // 在這里對i進行增加操作
                        i++;
                    } else {
                        // 說明沒有輪到當前線程打印辙喂,繼續(xù)wait
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "A").start();
        // B線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startB) {
                        System.out.print(Thread.currentThread().getName());
                        startA = false;
                        startB = false;
                        startC = true;
                        o.notifyAll();
                        i++;
                    } else {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "B").start();
        // C線程
        new Thread(() -> {
            synchronized (o) {
                for (int i = 0; i < 10; ) {
                    if (startC) {
                        System.out.print(Thread.currentThread().getName());
                        startA = true;
                        startB = false;
                        startC = false;
                        o.notifyAll();
                        i++;
                    } else {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "C").start();
    }
}

執(zhí)行結(jié)果如下:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

3捶牢、使用 Lock 搭配 Condition 實現(xiàn)

使用 synchronized 鎖機制的寫法著實有些復雜赃额,何不試試 ReentrantLock?這是java.util.concurrent.locks包下的鎖實現(xiàn)類叫确,它擁有更靈活的 API跳芳,能夠?qū)Χ嗑€程執(zhí)行流程實現(xiàn)更精細的控制,特別是在搭配 Condition 使用的情況下竹勉,可以隨心所欲地控制多個線程的執(zhí)行順序飞盆,來看看這個組合在本題中的使用吧,代碼如下:

public class PrintABC {
  public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        // 使用ReentrantLock的newCondition()方法創(chuàng)建三個Condition
        // 分別對應A次乓、B吓歇、C三個線程
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();

        // A線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    // 叫醒B線程
                    conditionB.signal();
                    // 本線程阻塞
                    conditionA.await();
                }
                // 這里有個坑,要記得在循環(huán)之后調(diào)用signal()票腰,否則線程可能會一直處于
                // wait狀態(tài)城看,導致程序無法結(jié)束
                conditionB.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 在finally代碼塊調(diào)用unlock方法
                lock.unlock();
            }
        }, "A").start();
        // B線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    conditionC.signal();
                    conditionB.await();
                }
                conditionC.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "B").start();
        // C線程
        new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    System.out.print(Thread.currentThread().getName());
                    conditionA.signal();
                    conditionC.await();
                }
                conditionA.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "C").start();
    }
}

執(zhí)行結(jié)果如下:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

4、使用 Semaphore 實現(xiàn)

semaphore中文意思是信號量杏慰,原本是操作系統(tǒng)中的概念测柠,JUC下也有個 Semaphore 的類,可用于控制并發(fā)線程的數(shù)量缘滥。Semaphore 的構造方法有個 int 類型的 permits 參數(shù)轰胁,如下:

public Semaphore(int permits) {...}

其中 permits 指的是該 Semaphore 對象可分配的許可數(shù),一個線程中的 Semaphore 對象調(diào)用acquire()方法可以讓線程獲取許可繼續(xù)運行朝扼,同時該對象的許可數(shù)減一赃阀,如果當前沒有可用許可,線程會阻塞擎颖。該 Semaphore 對象調(diào)用release()方法可以釋放許可榛斯,同時其許可數(shù)加一。Talk is cheap, show me the code!

public class PrintABC {
  
    public static void main(String[] args) {
        // 初始化許可數(shù)為1搂捧,A線程可以先執(zhí)行
        Semaphore semaphoreA  = new Semaphore(1);
        // 初始化許可數(shù)為0驮俗,B線程阻塞
        Semaphore semaphoreB  = new Semaphore(0);
        // 初始化許可數(shù)為0,C線程阻塞
        Semaphore semaphoreC  = new Semaphore(0);

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    // A線程獲得許可异旧,同時semaphoreA的許可數(shù)減為0,進入下一次循環(huán)時
                    // A線程會阻塞意述,知道其他線程執(zhí)行semaphoreA.release();
                    semaphoreA.acquire();
                    // 打印當前線程名稱
                    System.out.print(Thread.currentThread().getName());
                    // semaphoreB許可數(shù)加1
                    semaphoreB.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    semaphoreB.acquire();
                    System.out.print(Thread.currentThread().getName());
                    semaphoreC.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    semaphoreC.acquire();
                    System.out.print(Thread.currentThread().getName());
                    semaphoreA.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
    }
}

執(zhí)行結(jié)果如下:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

5、總結(jié)

本文一共介紹了四種三個線程交替打印的實現(xiàn)方法吮蛹,其中第一種方法最簡單易懂荤崇,但是更能考察多線程編程功底的應該是第二和第三種方法,在面試中也更加分潮针。只要把這幾種方法熟練掌握并徹底理解术荤,以后碰到此類題型就不用慌了。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末每篷,一起剝皮案震驚了整個濱河市瓣戚,隨后出現(xiàn)的幾起案子端圈,更是在濱河造成了極大的恐慌,老刑警劉巖子库,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舱权,死亡現(xiàn)場離奇詭異,居然都是意外死亡仑嗅,警方通過查閱死者的電腦和手機宴倍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仓技,“玉大人鸵贬,你說我怎么就攤上這事〔蹦恚” “怎么了阔逼?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長地沮。 經(jīng)常有香客問我嗜浮,道長,這世上最難降的妖魔是什么诉濒? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任周伦,我火速辦了婚禮,結(jié)果婚禮上未荒,老公的妹妹穿的比我還像新娘。我一直安慰自己及志,他們只是感情好片排,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著速侈,像睡著了一般率寡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上倚搬,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天冶共,我揣著相機與錄音,去河邊找鬼每界。 笑死捅僵,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的眨层。 我是一名探鬼主播庙楚,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼趴樱!你這毒婦竟也來了馒闷?” 一聲冷哼從身側(cè)響起酪捡,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纳账,沒想到半個月后逛薇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疏虫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年永罚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片议薪。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡尤蛮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斯议,到底是詐尸還是另有隱情产捞,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布哼御,位于F島的核電站坯临,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恋昼。R本人自食惡果不足惜看靠,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望液肌。 院中可真熱鬧挟炬,春花似錦、人聲如沸嗦哆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽老速。三九已至粥喜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間橘券,已是汗流浹背额湘。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旁舰,地道東北人锋华。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像鬓梅,于是被迫代替她去往敵國和親供置。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容