作為面試寵兒的多線程,在面試的時候是一定會被詢問的話題,今天,在和朋友聊天的時候,他問了我一道很好玩的多線程面試題,不難,但是想解釋清楚,還真的是不容易
問題:現在有T1、T2、T3三個線程,你怎樣保證T2在T1執行完后執行,T3在T2執行完后執行
當看到這個問題的時候,我的第一反應就是wait、notify(會在后面附上代碼+解釋),然后腦子里閃過好多不同的方案,那我們就來看一下我的第一反應是如何處理的
分析:
- Thread類中的join方法是用來同步的,底層其實是調用了 wait方法。先來看一下演示代碼:
package com.whh.concurrency; /** *@description: * 問題:現在有 T1、T2、T3 三個線程,怎樣保證 T2 在 T1 執行完后執行T3在T2執行完 * 分析:使用join方法實現 *@author:wenhuohuo */ public class MyJoin { public static void main(String[] args) { final Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("線程1"); } },"t1"); final Thread t2 = new Thread(new Runnable() { @Override public void run() { try { t1.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println("線程2"); } },"t2"); final Thread t3 = new Thread(new Runnable() { @Override public void run() { try { t2.join(); }catch (Exception e){ e.printStackTrace(); } System.out.println("線程3"); } },"t3"); t3.start(); t2.start(); t1.start(); } }
執行結果:
線程1
線程2
線程3
可以看到,我們讓t2線程調用t1.join,t3調用t2.join,盡管是t3,t2,t1分別start,執行順序還是t1,t2,t3。是因為join方法底層使用的是wait方法。
- 查看join方法源碼
public final void join() throws InterruptedException { join(0); //傳入的是毫秒值 }
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { //isAlive()是native方法,判斷線程是否還存活 wait(0); //wait(0),不計時間,一直等待,直到有notify()或notifyAll()來喚醒。 } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay);//傳入時間,表示在時間值消耗完之前一直等待,直到過了等待時間。 now = System.currentTimeMillis() - base; } } }
1)從源碼中,我們結合之前的代碼分析,t2.join()和t3.join(),均沒有傳值,相當於join(0),表示不計時間,t2會一直wait等待t1執行完成,t3會一直wait等待t2執行完成。所以執行結果順序是t3,t2,t1。
2)當傳入的毫秒值不為0時,就一直循環等待,直到過了等待時間(dalay<=0),則執行break方法,那么將不再等待。
- 改變join()傳入的毫秒值,查看執行順序並分析結果:
public class MyJoin { public static void main(String[] args) { final Thread t1 = new Thread(new Runnable() { @Override public void run() { try { //處理業務時間,模擬為8秒 Thread.sleep(8000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("線程1"); } },"t1"); final Thread t2 = new Thread(new Runnable() { @Override public void run() { try { t1.join(4000); //t2等待t1線程4秒 } catch (Exception e) { e.printStackTrace(); } System.out.println("線程2"); } },"t2"); final Thread t3 = new Thread(new Runnable() { @Override public void run() { try { t2.join(2000); //t3等待t2線程2秒 }catch (Exception e){ e.printStackTrace(); } System.out.println("線程3"); } },"t3"); t3.start(); t2.start(); t1.start(); } }
執行結果:
線程3 //程序啟動過了2秒執行t3 線程2 //過了4秒執行t2 線程1 //過了8秒執行t1
分析:我們讓t1 睡眠8秒模擬業務執行時間,t2等待t1 的時間為4秒,t3等待t2的時間為2秒。那么當t1,t2,t3啟動后,等待的時間,t3會因為t2的等待時間4秒太長而先與t2執行,t2會因為t1的8秒太長而先與t1執行。
好了,到這里不知道大家感覺怎么樣,有沒有想明白,其實,關於多線程順序執行的問題,我總結了一個萬能的解答方法,通過一個小例子進行演示
例題描述:建立三個線程,A線程打印100次A,B線程打印100次B,C線程打印100次C,要求線程同時運行,交替打印100次ABC。這個問題用Object的wait(),notify()就可以很方便的解決。
代碼如下:
public class QueueThread implements Runnable{ private Object current; private Object next; private int max=100; private String word; public QueueThread(Object current, Object next, String word) { this.current = current; this.next = next; this.word = word; } @Override public void run() { // TODO Auto-generated method stub for(int i=0;i<max;i++){ synchronized (current) { synchronized (next) { System.out.println(word);<span id="transmark"></span> next.notify(); } try { current.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //必須做一下這樣處理,否則thread1-thread4停不了 synchronized (next) { next.notify(); System.out.println(Thread.currentThread().getName()+"執行完畢"); } } public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); Object a = new Object(); Object b = new Object(); Object c = new Object(); Object d = new Object(); Object e = new Object(); //之所以每次當前線程都要sleep(10)是為了保證線程的執行順序 new Thread(new QueueThread(a,b,"a")).start(); Thread.sleep(10); new Thread(new QueueThread(b,c,"b")).start(); Thread.sleep(10); new Thread(new QueueThread(c,d,"c")).start(); Thread.sleep(10); new Thread(new QueueThread(d,e,"d")).start(); Thread.sleep(10); Thread thread4 = new Thread(new QueueThread(e,a,"e")); thread4.start(); thread4.join();//因為線程0-4停止是依次執行的,所以如果保證主線程在線程4后停止,那么就能保證主線程是最后關閉的 System.out.println("程序耗時:"+ (System.currentTimeMillis()-startTime )); } }
其實這個程序很容易理解,首先,我們保證了線程0-線程4依次啟動,並設置了Thread.sleep(10),保證線程0-4依次執行他們的run方法。
其次,我們看QueueThread的run()便可知:1.線程獲得current鎖,2.獲得next鎖。3.打印並notify擁有next鎖的一個對象4.線程執行current.wait(),釋放current鎖對象,並使線程處於阻塞狀態。
然后,假設已經執行到了thread-4的run方法,那么此時的情況是這樣的:
線程0處於阻塞狀態,需要a.notify()才能使其回到runnale狀態
線程1處於阻塞狀態,需要b.notify()才能使其回到runnale狀態
線程2處於阻塞狀態,需要c.notify()才能使其回到runnale狀態
線程3處於阻塞狀態,需要d.notify()才能使其回到runnale狀態
而線程4恰好可以需要執行a.notify(),所以能夠使線程0回到runnale狀態。然后執行e,wait()方法,使自身線程阻塞,需要e.notify()才能喚醒。
依次執行下去,就可以發現規律了!
最后之所以要在for循環后加上一個處理,是因為,如果不進行處理,除了線程0能夠結束for循環,其余線程1-4實際上是會停在current,wait()這句代碼的。
假設已經執行到最后一次循環了,此時線程4喚醒線程0,並將自身阻塞。線程0被喚醒后,繼續執行,然而因為i=max的緣故,它無法再進入循環了。然而如果循環后沒有喚醒下一個線程的操作的話,那么剩下的線程1-4就會一直處於阻塞狀態!也就不會停止了。但是加了處理之后就完美解決了。
好啦,到這里兩個小實例就講完了,不知道通過這兩個小實例有沒有讓你很好的理解這道面試題呢?有問題的話,下方評論區,大家一起討論吧
歡迎大家關注我,一個腦回路清奇的程序猿,在娛樂中學習是我的真諦,大家一起努力進步
