如何控制多線程執行順序


前言:這道經典的面試題其實考察的是面試者對多線程API的了解程度。如果不考慮線程的API方法的話,自己腦路大開的話,方法其實很多種。今天我們就提兩種最簡單,也是最常用到的方法。

目標:建三個線程分別為thread1,thread2,thread3,讓這三個線程依次執行。

首先,先來個多線程的實例:

package main.java;

public class App {

static Thread thread1 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread1");
}

});

static Thread thread2 = new Thread(new Runnable(){

@Override
public void run() {

System.out.println("thread2");
}

});

static Thread thread3 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread3");

}

});

public static void main(String[] args){

thread1.start();
thread2.start();
thread3.start();

}


}

 

執行結果如下:

thread1
thread2
thread3

多執行就幾次:

thread3
thread1
thread2

執行多次后發現:
我們執行相同代碼的結果是,每次運行結果都是隨機的。


在查閱了相關資料后,我發現了一些可怕的事實:

線程在啟動以后,並不是第一時間就會立馬執行。而是要等待CPU的一個資源調度,而CPU調度的順序呢是通過復雜算法計算得到的。等啟動的線程得到CPU指令后,才和主線程做一個切換,執行run方法。這就造成了每次我們執行的結果都是隨機的。

這里順便補充一下線程的狀態以及轉換,供各位參考:

說明:
線程共包括以下5種狀態。
1. 新建狀態(New) : 線程對象被創建后,就進入了新建狀態。例如,Thread thread = new Thread()。
2. 就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建后,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
3. 運行狀態(Running) : 線程獲取CPU權限進行執行。需要注意的是,線程只能從就緒狀態進入到運行狀態。
4. 阻塞狀態(Blocked) : 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
(01) 等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。
(02) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。
(03) 其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5. 死亡狀態(Dead) : 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

 

方法一:Join()使用。

先看一段代碼:

package main.java;

public class App {

static Thread thread1 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread1");
}

});

static Thread thread2 = new Thread(new Runnable(){

@Override
public void run() {

System.out.println("thread2");
}

});

static Thread thread3 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread3");

}

});

public static void main(String[] args) throws InterruptedException{

thread1.start();
thread1.join();

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

thread3.start();

}


}

運行結果如下:

thread1
thread2
thread3

 

這次我們不管執行多少次都是按順序執行的。
原理分析:
Join()作用:讓主線程等待子線程運行結束后才能繼續運行。
這段代碼里面的意思是這樣的:

程序在main線程中調用thread1線程的join方法,則main線程放棄cpu控制權,並返回thread1線程繼續執行直到線程thread1執行完畢
所以結果是thread1線程執行完后,才到主線程執行,相當於在main線程中同步thread1線程,thread1執行完了,main線程才有執行的機會

作為一個有素養的技術人,這里必須要親自看看join()的源碼了。

/**
* Waits at most <code>millis</code> milliseconds for this thread to
* die. A timeout of <code>0</code> means to wait forever.
*/

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;
}
}
}

源碼解讀:
這里有一個isAlive()方法很重要。什么意思呢?
判斷當前線程是否處於活動狀態。活動狀態就是線程啟動且尚未終止,比如正在運行或准備開始運行。

所以從代碼上看,如果線程被生成了,但還未被起動,調用它的 join() 方法是沒有作用的,將直接繼續向下執行。

wait()方法,什么意思呢?
在Object.java中,wait()的作用是讓當前線程進入等待狀態,同時,wait()也會讓當前線程釋放它所持有的鎖。

所以Join()主要就是通過wait()方法來實現這個目的的。

最后來個代碼步驟解讀吧:
1: 主線程運行;
2:創建thread1線程 (創建后的thread1線程狀態為新建狀態);
3:主線程調用thread1.start()方法 (thread1線程狀態變為就緒狀態,等待cpu的一個資源調度,有了資源后thread1狀態為運行狀態);
4:主線程調用thread1.join() 方法 (主線程會休眠,等待子線程thread1運行結束后才會繼續運行)。

方法二:ExecutorService ()的使用。

依舊先看代碼:

```
package main.java;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class App {

static Thread thread1 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread1");
}

});


static Thread thread2 = new Thread(new Runnable(){

@Override
public void run() {

System.out.println("thread2");
}

});

static Thread thread3 = new Thread(new Runnable(){

@Override
public void run() {
System.out.println("thread3");

}

});

static ExecutorService executorService = Executors.newSingleThreadExecutor();

public static void main(String[] args) throws InterruptedException{

executorService.submit(thread1);
executorService.submit(thread2);
executorService.submit(thread3);

executorService.shutdown();
}


}

```

運行結果如下:

```
thread1
thread2
thread3

```
結果:無論運行多少次,結果都是按照我們的順序執行的。

原理:利用並發包里的Excutors的newSingleThreadExecutor產生一個單線程的線程池,而這個線程池的底層原理就是一個先進先出(FIFO)的隊列。代碼中executor.submit依次添加了123線程,按照FIFO的特性,執行順序也就是123的執行結果,從而保證了執行順序。

這里的源碼就多了,展開來太多了,有興趣的可以自行了解一下。面試答到這兩個就基本差不多了。還有很多自定義的實現也可以做到同樣的效果,這里也不做展開。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM