摘要:基於如何讓多線程按照自己指定的順序執行這個場景,淺談Thread中join()函數的作用和原理。
join的作用
之前有人問過我一個這樣的面試題:如何讓多線程按照自己指定的順序執行?這個問題最簡單的回答是通過Thread.join來實現。
讓父線程等待子線程結束之后才能繼續運行。我們來看看在 Java 7 Concurrency Cookbook 中相關的描述(很清楚地說明了 join() 的作用):
Waiting for the finalization of a threadIn some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program. For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.
黑體部分英文的大意是當我們用某個線程調用這個方法時,join方法會掛起調用線程,直到被調用線程結束執行,調用線程才會繼續執行。下面用一個示例驗證一下:
public class JoinDemo extends Thread {
int i;
Thread previousThread; //上一個線程
public JoinDemo(Thread previousThread, int i) {
this.previousThread = previousThread;
this.i = i;
}
@Override
public void run() {
try {
//調用上一個線程的join方法,大家可以自己演示的時候可以把這行代碼注釋掉
previousThread.join();
Object aa = new Object();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(previousThread.getName() + ", num:" + i);
}
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
previousThread.setName("parent thread");
for (int i = 0; i < 10; i++) {
JoinDemo joinDemo = new JoinDemo(previousThread, i);
joinDemo.start();
previousThread = joinDemo;
previousThread.setName("child thread " + i);
}
}
}
上面的代碼,注意 previousThread.join()部分,大家可以把這行代碼注釋以后看看運行效果,在沒有加join的時候運行的結果是不確定的,加了join以后運行結果按照遞增的順序輸出。
源碼分析
public class Thread implements Runnable {
...
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) { //判斷是否攜帶阻塞的超時時間,等於0表示沒有設置超時時間
while (isAlive()) {//isAlive獲取線程狀態,無線等待直到previousThread線程結束
wait(0); //調用Object中的wait方法實現線程的阻塞
}
} else { //阻塞直到超時
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
...
由此可見,join()方法是通過調用Object中的wait()函數實現線程的阻塞。main線程在調用previousThread.join()時,會持有線程對象previousThread的鎖(wait 意味着拿到該對象的鎖),然后調用previousThread的wait方法造成主線程阻塞,直到該對象喚醒main線程(即子線程previousThread執行完畢退出的時候)。synchronized修飾在方法層面相當於synchronized(this),this就是previousThread本身的實例。
子線程結束后,子線程previousThread的this.notifyAll()會被調用,join()返回,主線程只要獲取到鎖和CPU執行權,就可以繼續執行了。
小結
首先join() 是一個synchronized方法, 里面調用了wait(),這個過程的目的是讓持有這個同步鎖的線程進入等待,那么誰持有了這個同步鎖呢?答案是主線程,因為主線程調用了previousThread.join()方法,相當於在previousThread.join()代碼這塊寫了一個同步代碼塊,誰去執行了這段代碼呢?是主線程,所以主線程被wait()了。然后在子線程previousThread執行完畢之后,JVM會調用lock.notify_all(thread);喚醒持有previousThread這個對象鎖的線程,也就是主線程,使主線程繼續執行。
Thread.join使用場景
在實際使用過程中,我們可以通過join方法來等待線程執行的結果,其實有點類似future/callable的功能。通過以下偽代碼來說明join的使用場景:
public void joinDemo(){
//....
Thread t=new Thread(payService);
t.start();
//....
//其它業務邏輯處理,不需要確定t線程是否執行完
insertData();
//后續的處理,需要依賴t線程的執行結果,可以在這里調用join方法等待t線程的執行結束
t.join();
}
