面試的時候你是否經常被問到這樣的問題:
你一般通過什么方式去控制線程的執行順序?
碰到這樣的問題,我的內心其實是很抵觸的!
開什么玩笑?我怎么會控制它呢?我為什么要控制它?
其實不用慌,這個問題並不難,且聽我慢慢道來......
那么,什么是線程和進程?
要想控制多線程的順序,你首先應該搞清楚線程和進程到底是什么東西?
進程
進程其實是操作系統的基礎,是系統中一次程序的執行,也是一次程序和數據在機器上順序執行時所發生的活動,又是系統進行資源分配和調度的一個獨立單位。
其實說的通俗一點,可以這么理解,進程就是Windows系統中執行的一個exe程序,是操作系統管理的基本運行單元,看下面這個圖你就知道啥是進程了!
線程
線程是比進程還要小的一個單元,它是進程中獨立運行的子任務。
你比如說一個微信.exe程序進程中就有非常多的子線程在同時運行,例如音視頻線程、文件下載線程、信息傳輸線程等等,這些不同的任務如果都在一個線程去運行,那程序必定會特別慢,大家的體驗不會像現在那樣舒服,所以這里的每一個任務或功能都需要對應一個后台的線程在默默運行。
簡單來說,線程就是組成進程的一條路徑,一個進程可以包含一個或者多個線程。
什么是多線程環境?
關於多線程的環境,其實大家在使用Windows系統的時候就深有感觸。
想象你一邊在用IDE碼代碼,一邊要和朋友聊天,還一邊帶着耳機聽着音樂,你的系統為什么能夠同時提供給你那么多服務呢?
其實這就是一個典型的多線程環境,你的電腦的CPU正在不斷的從這些任務中飛速切換來處理程序中的各種事情,由於計算機的切換處理速度非常的快,所以你沒辦法在界面上進行感知,給我們的感覺就是他們其實在同時為我們服務着,這也是多線程環境的一種優勢!
用一張圖的方式來對比感受一下多線程的環境,例如:
在上圖中,單線程環境下一號、二號處理任務是完全獨立的兩個任務,但是二號任務必須要等待1號任務處理完成才能執行,也就是二號處理任務必須要在程序開始后的5秒后才能運行到,最終的運行時間為15秒。
但是在多線程環境下,一號、二號雖然也是完全獨立的任務,處在同一個進程中,但卻由不同的線程去處理,CPU可以在這兩個不同的線程之間進行切換,所以二號處理任務不需要在一號處理完成之后再處理,而是做異步處理,最終的運行時間也差不多在10秒左右。
幾個關於CPU、線程執行的知識點
1、在單CPU計算機中,CPU是無法被多個程序並行使用的。
2、操作系統中存在一種調度器,它可以負責拆分CPU為一段段時間的運行片,輪流分配給不同的進程。
3、程序的運行不僅僅需要CPU,還需要很多其他資源,如內存啊,顯卡啊,GPS啊,磁盤等等,這些統稱為程序的執行環境,也就是程序上下文。
4、多個程序沒辦法同一個時間共享CPU,那怎么辦呢?這個時候比進程更小的線程就出來了,通過在不同線程的切換來達到共享CPU、共享程序上下文的目的。
5、大家都知道,CPU有單核和多核區別,單核CPU其實就是多個線程會輪流得到那一個CPU核心的支持;在多核CPU中,一個核心可以服務於一個線程,例如我的電腦是4核的話,有四個線程A、B、C、D需要處理,那CPU會將他們分配到核心1、2、3、4,如果還有其他更多的線程,也必須要等待CPU的切換執行。
通過上面幾個知識點,可以看出不管是在多核還是單核的系統中,CPU在多線程的環境中都是要不斷切換線程來處理任務的,當然,CPU在切換任務時也不是順便切換,而是根據一定的算法來調度、切換線程,一般有這樣兩種模式:分時調度和搶占式調度。
分時調度就是按照順序平均分配;
搶占式調度就是按照優先級來進行分配。
具體的算法邏輯在這里就不再詳細描述,有疑問的小伙伴們可以再繼續往下深入探索......
如何去控制多線程的執行順序?
通過一個簡單的程序,體驗一下線程是否會隨機被執行,手動創建5個Thread對象,然后讓他們按照順序開啟,如下面代碼:
/**
* 多線程Test
*/
public class Main {
static Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread01");
}
});
static Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread02");
}
});
static Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread03");
}
});
static Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread04");
}
});
static Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread05");
}
});
public static void main(String[] args) {
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
最后執行的結果大家一定已經猜到了,多次執行,它的順序並不是固定的,而是隨機在改變的,例如:
那么在多線程的環境中,我們有時候不想讓CPU根據算法隨機選取任務執行,而是想控制多線程的執行順序,那應該如何操作呢?
目前我知道的主要有兩種方法:
1、join方式
我們直接通過在每個Thread對象后面使用join方法就可以實現線程的順序執行,代碼如下:
public static void main(String[] args) throws Exception {
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
thread4.start();
thread4.join();
thread5.start();
thread5.join();
}
多次執行結果都為下面這種情況:
用join方法來保證線程順序,其實就是讓main這個主線程等待子線程結束,然后主線程再執行接下來的其他線程任務,點進去join方法我們可以了解的更透徹:
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} 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;
}
}
}
源碼中的參數millis默認值是0,從英文注釋翻譯后可以找到,0秒意味着永遠等待,也就是thread1執行不完,那主線程你就要一直等着,一直wait,而代碼中wait方法其實就是屬於Object的方法,負責線程的休眠等待,當main主線程調用thread1.join的時候,main主線程會獲得線程對象thread1的鎖(wait 意味着拿到該對象的鎖),調用該對象的wait(等待時間),直到該對象喚醒main主線程 ,比如退出后。這就意味着main 線程調用thread1.join時,必須能夠拿到線程t對象的鎖。
2、ExecutorService方式
首先看一下代碼,我們如何通過這種方式實現線程順序執行:
static ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args) throws Exception {
executorService.submit(thread1);
executorService.submit(thread2);
executorService.submit(thread3);
executorService.submit(thread4);
executorService.submit(thread5);
executorService.shutdown();
}
最終的多次執行結果均為有序的,如下圖:
解釋一下,這種方式的原理其實就是將線程用排隊的方式扔進一個線程池里,讓所有的任務以單線程的模式,按照FIFO先進先出、LIFO后進先出、優先級等特定順序執行,但是這種方式也是存在缺點的,就是當一個線程被阻塞時,其它的線程都會受到影響被阻塞,不過依然都會按照自身調度來執行,只是會存在阻塞延遲。
總結
總之,如果面試官真的問到大家如何控制多線程執行順序的方法,就按照上面的兩種方式回答即可,當然,面試官既然問到這個問題,就並不只是看大家是否知道這一個問題的具體答案,可能會刨根問底的讓你回答更深入的一些多線程問題,所以在日常的學習過程中一定要重在積累,勤於探索,上面提到的也只是我研究到的皮毛。
最后真心希望,在以后的技術之路跟大家一起成長!