「高并發(fā)」線程到底是按照怎樣的順序執(zhí)行的?
最近經(jīng)常有讀者問我:冰河,線程到底是按照怎樣的順序執(zhí)行的呀?為了同一回答大家的這個問題,今天我就單獨寫一篇文章吧。好了,不多說了,進入今天的正題。
一、線程的執(zhí)行順序是不確定的
調(diào)用Thread的start()方法啟動線程時,線程的執(zhí)行順序是不確定的。也就是說,在同一個方法中,連續(xù)創(chuàng)建多個線程后,調(diào)用線程的start()方法的順序并不能決定線程的執(zhí)行順序。
例如,在這里,看一個簡單的示例程序,如下所示。
package io.binghe.concurrent.lab03;
/**
* @author binghe
* @version 1.0.0
* @description 線程的順序,直接調(diào)用Thread.start()方法執(zhí)行不能確保線程的執(zhí)行順序
*/
public class ThreadSort01 {
public static void main(String[] args){
Thread thread1 = new Thread(() -> {
System.out.println("thread1");
});
Thread thread2 = new Thread(() -> {
System.out.println("thread2");
});
Thread thread3 = new Thread(() -> {
System.out.println("thread3");
});
thread1.start();
thread2.start();
thread3.start();
}
}
在ThreadSort01類中分別創(chuàng)建了三個不同的線程,thread1、thread2和thread3,接下來,在程序中按照順序分別調(diào)用thread1.start()、thread2.start()和thread3.start()方法來分別啟動三個不同的線程。
那么,問題來了,線程的執(zhí)行順序是否按照thread1、thread2和thread3的順序執(zhí)行呢?運行ThreadSort01的main方法,結(jié)果如下所示。
thread1
thread2
thread3
再次運行時,結(jié)果如下所示。
thread1
thread3
thread2
第三次運行時,結(jié)果如下所示。
thread2
thread3
thread1
可以看到,每次運行程序時,線程的執(zhí)行順序可能不同。線程的啟動順序并不能決定線程的執(zhí)行順序。
二、如何確保線程的執(zhí)行順序
1.確保線程執(zhí)行順序的簡單示例
在實際業(yè)務場景中,有時,后啟動的線程可能需要依賴先啟動的線程執(zhí)行完成才能正確地執(zhí)行線程中的業(yè)務邏輯。此時,就需要確保線程的執(zhí)行順序。那么如何確保線程的執(zhí)行順序呢?
可以使用Thread類中的join()方法來確保線程的執(zhí)行順序。例如,下面的測試代碼。
package io.binghe.concurrent.lab03;
/**
* @author binghe
* @version 1.0.0
* @description 線程的順序,Thread.join()方法能夠確保線程的執(zhí)行順序
*/
public class ThreadSort02 {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("thread1");
});
Thread thread2 = new Thread(() -> {
System.out.println("thread2");
});
Thread thread3 = new Thread(() -> {
System.out.println("thread3");
});
thread1.start();
//實際上讓主線程等待子線程執(zhí)行完成
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
}
}
可以看到,ThreadSot02類比ThreadSort01類,在每個線程的啟動方法下面添加了調(diào)用線程的join()方法。此時,運行ThreadSort02類,結(jié)果如下所示。
thread1
thread2
thread3
再次運行時,結(jié)果如下所示。
thread1
thread2
thread3
第三次運行時,結(jié)果如下所示。
thread1
thread2
thread3
可以看到,每次運行的結(jié)果都是相同的,所以,使用Thread的join()方法能夠保證線程的先后執(zhí)行順序。
2.join方法如何確保線程的執(zhí)行順序
既然Thread類的join()方法能夠確保線程的執(zhí)行順序,我們就一起來看看Thread類的join()方法到底是個什么鬼。
進入Thread的join()方法,如下所示。
public final void join() throws InterruptedException {
join(0);
}
可以看到j(luò)oin()方法調(diào)用同類中的一個有參join()方法,并傳遞參數(shù)0。繼續(xù)跟進代碼,如下所示。
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()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可以看到,有一個long類型參數(shù)的join()方法使用了synchroinzed修飾,說明這個方法同一時刻只能被一個實例或者方法調(diào)用。由于,傳遞的參數(shù)為0,所以,程序會進入如下代碼邏輯。
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
首先,在代碼中以while循環(huán)的方式來判斷當前線程是否已經(jīng)啟動處于活躍狀態(tài),如果已經(jīng)啟動處于活躍狀態(tài),則調(diào)用同類中的wait()方法,并傳遞參數(shù)0。繼續(xù)跟進wait()方法,如下所示。
public final native void wait(long timeout) throws InterruptedException;
可以看到,wait()方法是一個本地方法,通過JNI的方式調(diào)用JDK底層的方法來使線程等待執(zhí)行完成。
需要注意的是,調(diào)用線程的wait()方法時,會使主線程處于等待狀態(tài),等待子線程執(zhí)行完成后再次向下執(zhí)行。也就是說,在ThreadSort02類的main()方法中,調(diào)用子線程的join()方法,會阻塞main()方法的執(zhí)行,當子線程執(zhí)行完成后,main()方法會繼續(xù)向下執(zhí)行,啟動第二個子線程,并執(zhí)行子線程的業(yè)務邏輯,以此類推。