【高併發】深入理解執行緒的執行順序

語言: CN / TW / HK

大家好,我是冰河~~

最近經常有讀者問我:冰河,執行緒到底是按照怎樣的順序執行的呀?為了同一回答大家的這個問題,今天我就單獨寫一篇文章吧。好了,不多說了,進入今天的正題。

一、執行緒的執行順序是不確定的

呼叫Thread的start()方法啟動執行緒時,執行緒的執行順序是不確定的。也就是說,在同一個方法中,連續建立多個執行緒後,呼叫執行緒的start()方法的順序並不能決定執行緒的執行順序。

例如,這裡,看一個簡單的示例程式,如下所示。

package io.binghe.concurrent.lab03;

/**
 * @author binghe
 * @version 1.0.0
 * @description 執行緒的順序,直接呼叫Thread.start()方法執行不能確保執行緒的執行順序
 */
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類中分別建立了三個不同的執行緒,thread1、thread2和thread3,接下來,在程式中按照順序分別呼叫thread1.start()、thread2.start()和thread3.start()方法來分別啟動三個不同的執行緒。

那麼,問題來了,執行緒的執行順序是否按照thread1、thread2和thread3的順序執行呢?執行ThreadSort01的main方法,結果如下所示。

thread1
thread2
thread3

再次執行時,結果如下所示。

thread1
thread3
thread2

第三次執行時,結果如下所示。

thread2
thread3
thread1

可以看到,每次執行程式時,執行緒的執行順序可能不同。執行緒的啟動順序並不能決定執行緒的執行順序。

二、如何確保執行緒的執行順序

1.確保執行緒執行順序的簡單示例

在實際業務場景中,有時,後啟動的執行緒可能需要依賴先啟動的執行緒執行完成才能正確的執行執行緒中的業務邏輯。此時,就需要確保執行緒的執行順序。那麼如何確保執行緒的執行順序呢?

可以使用Thread類中的join()方法來確保執行緒的執行順序。例如,下面的測試程式碼。

package io.binghe.concurrent.lab03;

/**
 * @author binghe
 * @version 1.0.0
 * @description 執行緒的順序,Thread.join()方法能夠確保執行緒的執行順序
 */
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();

        //實際上讓主執行緒等待子執行緒執行完成
        thread1.join();

        thread2.start();
        thread2.join();

        thread3.start();
        thread3.join();
    }
}

可以看到,ThreadSot02類比ThreadSort01類,在每個執行緒的啟動方法下面添加了呼叫執行緒的join()方法。此時,執行ThreadSort02類,結果如下所示。

thread1
thread2
thread3

再次執行時,結果如下所示。

thread1
thread2
thread3

第三次執行時,結果如下所示。

thread1
thread2
thread3

可以看到,每次執行的結果都是相同的,所以,使用Thread的join()方法能夠保證執行緒的先後執行順序。

2.join方法如何確保執行緒的執行順序

既然Thread類的join()方法能夠確保執行緒的執行順序,我們就一起來看看Thread類的join()方法到底是個什麼鬼。

進入Thread的join()方法,如下所示。

public final void join() throws InterruptedException {
    join(0);
}

可以看到join()方法呼叫同類中的一個有參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()) {
			wait(0);
		}
	} else {
		while (isAlive()) {
			long delay = millis - now;
			if (delay <= 0) {
				break;
			}
			wait(delay);
			now = System.currentTimeMillis() - base;
		}
	}
}

可以看到,有一個long型別引數的join()方法使用了synchroinzed修飾,說明這個方法同一時刻只能被一個例項或者方法呼叫。由於,傳遞的引數為0,所以,程式會進入如下程式碼邏輯。

if (millis == 0) {
	while (isAlive()) {
		wait(0);
	}
}

首先,在程式碼中以while迴圈的方式來判斷當前執行緒是否已經啟動處於活躍狀態,如果已經啟動處於活躍狀態,則呼叫同類中的wait()方法,並傳遞引數0。繼續跟進wait()方法,如下所示。

public final native void wait(long timeout) throws InterruptedException;

可以看到,wait()方法是一個本地方法,通過JNI的方式呼叫JDK底層的方法來使執行緒等待執行完成。

需要注意的是,呼叫執行緒的wait()方法時,會使主執行緒處於等待狀態,等待子執行緒執行完成後再次向下執行。也就是說,在ThreadSort02類的main()方法中,呼叫子執行緒的join()方法,會阻塞main()方法的執行,當子執行緒執行完成後,main()方法會繼續向下執行,啟動第二個子執行緒,並執行子執行緒的業務邏輯,以此類推。