JUC學習筆記——進程與執行緒
JUC學習筆記——進程與執行緒
在本系列內容中我們會對JUC做一個系統的學習,本片將會介紹JUC的進程與執行緒部分
我們會分為以下幾部分進行介紹:
- 進程與執行緒
- 並發與並行
- 同步與非同步
- 執行緒詳解
進程與執行緒
在這一小節我們將簡單介紹進程與執行緒
進程
首先我們來簡單了解一下程式:
- 程式由指令和數據組成,我們必須將指令載入至 CPU,數據載入至記憶體。在指令運行過程中還需要用到磁碟、網路等設備。
接下來我們才能講解進程的定義:
- 進程就是用來載入指令、管理記憶體、管理 IO 的
- 當一個程式被運行,從磁碟載入這個程式的程式碼至記憶體,這時就開啟了一個進程。
- 進程就可以視為程式的一個實例。大部分程式可以同時運行多個實例進程,也有的程式只能啟動一個實例進程。
執行緒
我們來簡單介紹一下執行緒:
- 一個進程之內可以分為一到多個執行緒。
- 一個執行緒就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執行
- Java 中,執行緒作為最小調度單位,進程作為資源分配的最小單位。 在 windows 中進程是不活動的,只是作為執行緒的容器
兩者區別
我們來介紹一下進程與執行緒之間的區別:
-
進程基本上相互獨立的,而執行緒存在於進程內,是進程的一個子集
-
進程擁有共享的資源,如記憶體空間等,供其內部的執行緒共享
-
執行緒更輕量,執行緒上下文切換成本一般上要比進程上下文切換低
此外兩者的通訊方式也不相同:
- 進程通訊:同一台電腦的進程通訊稱為 IPC;不同電腦之間的進程通訊,需要通過網路,並遵守共同的協議,例如 HTTP
- 執行緒通訊:執行緒通訊相對簡單,因為它們共享進程內的記憶體,一個例子是多個執行緒可以訪問同一個共享變數
並發與並行
在這一小節我們將簡單介紹並發與並行
並發
首先我們需要了解一下任務調度器:
- 單核 cpu 下,執行緒實際還是 串列執行 的。
- 作業系統中有一個組件叫做任務調度器,將 cpu 的時間片分給不同的程式使用。
那麼我們的並發實際上就是根據任務調度器來工作的:
- 並發是藉助任務調度器,在一段時間段內將CPU分給多個執行緒使用,但由於切換快時間短,所以被看作是同時進行
- 一般會將這種 執行緒輪流使用 CPU 的做法稱為並發
- 實際上並發的情況是:微觀串列,宏觀並行
通俗來講:
- 並發(concurrent)是同一時間應對(dealing with)多件事情的能力
例如下圖:
CPU | 時間片 1 | 時間片 2 | 時間片 3 | 時間片 4 |
---|---|---|---|---|
core | 執行緒 1 | 執行緒 2 | 執行緒 3 | 執行緒 4 |
並行
並行的概念就相對而言比較簡單了:
- 並行就是藉助多核CPU,確確實實地在同一時間執行多個進程
通俗來講:
- 並行(parallel)是同一時間動手做(doing)多件事情的能力
例如下圖:
CPU | 時間片 1 | 時間片 2 | 時間片 3 | 時間片 4 |
---|---|---|---|---|
core 1 | 執行緒 1 | 執行緒 1 | 執行緒 3 | 執行緒 3 |
core 2 | 執行緒 2 | 執行緒 4 | 執行緒 2 | 執行緒 4 |
同步與非同步
在這一小節我們將簡單介紹並發與並行
同步與非同步概念
首先我們來簡單介紹一下同步與非同步:
- 需要等待結果返回才能繼續執行的操作就是同步操作
- 不需要等待結果返回就可以繼續執行的操作就是非同步操作
另外同步操作還有另一個概念:
- 在多執行緒中,表示多執行緒的步調一致
同步與非同步選擇方法
我們的同步與非同步的選擇通常會決定程式的運行速度,因而選擇同步或非同步是非常重要的
我們先來介紹同步與非同步的實現方式:
- 同步就是在一個執行緒內完全執行所有命令
- 非同步可以在多執行緒中實現,當一個執行緒執行複雜操作比較耗時時,另一個執行緒可以執行其他簡單操作
我們再來介紹同步與非同步的選擇方法:
- 針對比較繁瑣的操作,我們通常會單獨創建一個新執行緒來進行處理,避免阻塞主執行緒
- Tomcat裡面的非同步Servlet也是非同步操作,讓用戶執行緒處理耗時較長的操作,避免阻塞Tomcat的工作執行緒
- UI程式中,開執行緒進行其他操作,避免UI執行緒
我們分別給出同步非同步的程式碼展示:
// 同步程式碼
package cn.itcast.n2;
import cn.itcast.Constants;
import cn.itcast.n2.util.FileReader;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
FileReader.read(Constants.MP4_FULL_PATH);
log.debug("do other things ...");
}
}
// 非同步程式碼
package cn.itcast.n2;
import cn.itcast.Constants;
import cn.itcast.n2.util.FileReader;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Async")
public class Async {
public static void main(String[] args) {
new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
log.debug("do other things ...");
}
}
同步與非同步實際使用
我們通常採用非同步操作來實現應用的速度提升:
// 例如我們有下面三個操作
計算 1 花費 10 ms
計算 2 花費 11 ms
計算 3 花費 9 ms
匯總需要 1 ms
如果我們採用主執行緒的同步操作來實現:
// 如果是串列執行,那麼總共花費的時間是 10 + 11 + 9 + 1 = 31ms
但是如果我們採用三個CPU的非同步操作來實現:
// 但如果是四核 cpu,各個核心分別使用執行緒 1 執行計算 1,執行緒 2 執行計算 2,執行緒 3 執行計算 3
// 那麼 3 個執行緒是並行的,花費時間只取決於最長的那個執行緒運行的時間,即 11ms 最後加上匯總時間只會花費 12ms
下面我們給出同步非同步的實際使用規則:
- 多核多執行緒速度快於單核多執行緒(非同步進行速度較快)
- 單核多執行緒速度慢於單核單執行緒(執行緒切換也需要耗費時間)
但是單核多執行緒也並非是一無是處:
- 單核 cpu 下,多執行緒不能實際提高程式運行效率,只是為了能夠在不同的任務之間切換
- 不同執行緒輪流使用cpu ,不至於一個執行緒總佔用 cpu,別的執行緒沒法幹活
此外多核 cpu 可以並行跑多個執行緒,但能否提高程式運行效率還是要分情況的 :
- 有些任務,經過精心設計,將任務拆分,並行執行,當然可以提高程式的運行效率。但不是所有計算任務都能拆分
- 也不是所有任務都需要拆分,任務的目的如果不同,談拆分和效率沒啥意義
最後就是我們的IO操作部分:
- IO 操作不佔用 cpu,只是我們一般拷貝文件使用的是【阻塞 IO】
- 這時相當於執行緒雖然不用 cpu,但需要一直等待 IO 結束,沒能充分利用執行緒
- 所以才有後面的【非阻塞 IO】和【非同步 IO】優化
執行緒詳解
這一小節我們將詳細介紹執行緒的具體內容
創建和運行執行緒
我們下面將介紹三種創建和運行執行緒的方法
直接使用 Thread
我們可以直接使用Thread來創建和運行執行緒:
// 創建執行緒對象
Thread t = new Thread() {
public void run() {
// 要執行的任務
}
};
// 啟動執行緒
t.start();
我們再給出一個實際例子:
// 構造方法的參數是給執行緒指定名字,推薦
Thread t1 = new Thread("t1") {
@Override
// run 方法內實現了要執行的任務
public void run() {
log.debug("hello");
}
};
t1.start();
我們給出實際輸出結果:
// 我們會注意到:前面標記了[t1]執行緒~
19:19:00 [t1] c.ThreadStarter - hello
使用 Runnable 配合 Thread
這裡我們將Thread裡面的方法採用Runnable類型的方法來代替:
// 創建Runnable類型的方法
Runnable runnable = new Runnable() {
public void run(){
// 要執行的任務
}
};
// 創建執行緒對象
Thread t = new Thread( runnable );
// 啟動執行緒
t.start();
我們給出一個實際例子:
// 創建任務對象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 參數1 是任務對象; 參數2 是執行緒名字,推薦
Thread t2 = new Thread(task2, "t2");
t2.start();
其結果為:
// 結果正常
9:19:00 [t2] c.ThreadStarter - hello
除此之外,我們在JDK8之後,我們可以採用函數式介面Lambda來簡化Runnable的書寫:
// 創建任務對象
Runnable task2 = () -> log.debug("hello");
// 參數1 是任務對象; 參數2 是執行緒名字,推薦
Thread t2 = new Thread(task2, "t2");
t2.start();
甚至我們都不用定義task,來直接採用Lambda方法書寫Thread中的task:
// 參數1 是任務對象; 參數2 是執行緒名字,推薦
Thread t2 = new Thread(() -> log.debug("hello"), "t2");
t2.start();
底層簡單解釋:
- 至於Thread為什麼能夠直接調用Runnable
- Thread在接收Runnable類型後,會將其賦值在this.target
- 而Thread的run方法會先來判斷是否存在target,如果存在就直接採用target方法
最後我們介紹一下使用Runnable的好處:
- 方法1 是把執行緒和任務合併在了一起,方法2 是把執行緒和任務分開了
- 用 Runnable 更容易與執行緒池等高級API 配合
- 用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活
使用FutureTask 配合 Thread(了解即可)
FutureTask 能夠接收 Callable 類型的參數,用來處理有返回結果的情況:
// 創建任務對象(Integer是返回對象)
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 參數1 是任務對象; 參數2 是執行緒名字,推薦
new Thread(task3, "t3").start();
// 主執行緒阻塞,同步等待 task 執行完畢的結果
Integer result = task3.get();
log.debug("結果是:{}", result);
我們給出結果:
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 結果是:100
我們給出簡單解釋:
- FutureTask內置了一個Callable對象,初始化方法將指定的Callable賦給這個對象。
- FutureTask實現了Runnable介面,並重寫了Run方法,在Run方法中調用了Callable中的call方法,並將返回值賦值給outcome變數
- get方法就是取出outcome的值。
多執行緒運行狀況
我們給出單核CPU運行多執行緒時不斷切換進程的狀況展示:
// 下述的操作不受我們控制,誰先調用,調用多久都不是我們管控的
@Slf4j(topic = "c.TestMultiThread")
public class TestMultiThread {
public static void main(String[] args) {
new Thread(() -> {
while(true) {
log.debug("running");
}
},"t1").start();
new Thread(() -> {
while(true) {
log.debug("running");
}
},"t2").start();
}
}
我們直接給出結果展示:
23:45:26.254 c.TestMultiThread [t2] - running
23:45:26.254 c.TestMultiThread [t2] - running
23:45:26.254 c.TestMultiThread [t2] - running
23:45:26.254 c.TestMultiThread [t2] - running
23:45:26.254 c.TestMultiThread [t1] - running
23:45:26.254 c.TestMultiThread [t1] - running
23:45:26.254 c.TestMultiThread [t1] - running
23:45:26.254 c.TestMultiThread [t1] - running
23:45:26.254 c.TestMultiThread [t1] - running
23:45:26.254 c.TestMultiThread [t1] - running
查看進程執行緒方法
由於不同系統的查看方法不同,我們主要介紹三種類型查看方法
Window
- 任務管理器可以查看進程和執行緒數,也可以用來殺死進程
- tasklist 查看進程 :tasklist| findstr (查找關鍵字)
- taskkill 殺死進程:taskkill /F(徹底殺死)/PID(進程PID)
Linux
- ps -fe 查看所有進程
- ps -fT -p 查看某個進程(PID)的所有執行緒
- kill 殺死進程 top 按大寫 H 切換是否顯示執行緒
- top -H -p 查看某個進程(PID)的所有執行緒
Java
- jps 命令查看所有 Java 進程
- jstack 查看某個 Java 進程(PID)的所有執行緒狀態
- jconsole 來查看某個 Java 進程中執行緒的運行情況(圖形介面)
執行緒運行底層解釋
我們將會介紹兩個與執行緒底層運行相關的原理
棧與棧幀
下面我們來介紹一下與進程息息相關的底層原理:
- 棧:存放棧幀的個體,每個執行緒具有一個單獨的棧來存放多個棧幀
- 棧幀:方法的全部記憶體佔用,每個方法使用時的全部記憶體都用一個棧幀來表示
同時棧和棧幀也有一定限制:
- 每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所佔用的記憶體
- 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法
我們給出一個簡單的程式碼展示:
// 這裡展現的是單執行緒,目前只有一個棧!
package cn.itcast.n3;
public class TestFrames {
// 首先main方法被調用,所以main方法先進入棧中,在main方法執行結束後被拋出棧
public static void main(String[] args) {
method1(10);
}
// 由於main方法調用了method1,所以棧中在存有main棧幀的同時也將method1棧幀調入,在method1方法執行完畢後拋出
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
// 由於method1方法調用了method2,所以棧中在存有main,method1棧幀的同時也將method2棧幀調入,method2方法執行完畢後拋出
private static Object method2() {
Object n = new Object();
return n;
}
}
// 這裡展現的是多執行緒,主執行緒和執行緒t1獨自各佔有一個棧,互不影響!
package cn.itcast.n3;
public class TestFrames {
// 這裡會產生兩個棧,兩個棧互不影響,兩個棧都會順序調用main,method1,method2棧幀,順序不定
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
method1(20);
}
};
t1.setName("t1");
t1.start();
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
執行緒上下文切換
我們再來介紹一下上下文切換:
- 當出現一些狀況時,系統會自動將CPU的使用權進行切換,交付給不同的執行緒進行使用
上下文切換措施:
- 要由作業系統保存當前執行緒的狀態,並恢復另一個執行緒的狀態,
- Java 中對應的就是程式計數器,它的作用是記住下一條 jvm 指令的執行地址,是執行緒私有的
- 狀態包括程式計數器、虛擬機棧中每個棧幀的資訊,如局部變數、操作數棧、返回地址等
上下文切換時機:
- 執行緒的 cpu 時間片用完
- 垃圾回收
- 有更高優先順序的執行緒需要運行
- 執行緒自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法
但是我們需要注意:
- Context Switch 頻繁發生會影響性能
執行緒方法詳解
這一小節我們將介紹執行緒的各種方法
執行緒方法總述
我們首先給出執行緒的全部方法一覽:
方法 | 功能 | 說明 |
---|---|---|
public void start() | 啟動一個新執行緒;Java虛擬機調用此執行緒的run方法 | start 方法只是讓執行緒進入就緒,裡面程式碼不一定立刻 運行(CPU 的時間片還沒分給它)。每個執行緒對象的 start方法只能調用一次,如果調用了多次會出現 IllegalThreadStateException |
public void run() | 執行緒啟動後調用該方法 | 如果在構造 Thread 對象時傳遞了 Runnable 參數,則 執行緒啟動後會調用 Runnable 中的 run 方法,否則默 認不執行任何操作。但可以創建 Thread 的子類對象, 來覆蓋默認行為 |
public void setName(String name) | 給當前執行緒取名字 | |
public void getName() | 獲取當前執行緒的名字。執行緒存在默認名稱:子執行緒是Thread-索引,主執行緒是main | |
public static Thread currentThread() | 獲取當前執行緒對象,程式碼在哪個執行緒中執行 | |
public static void sleep(long time) | 讓當前執行緒休眠多少毫秒再繼續執行。Thread.sleep(0) : 讓作業系統立刻重新進行一次cpu競爭 | |
public static native void yield() | 提示執行緒調度器讓出當前執行緒對CPU的使用 | 主要是為了測試和調試 |
public final int getPriority() | 返回此執行緒的優先順序 | |
public final void setPriority(int priority) | 更改此執行緒的優先順序,常用1 5 10 | java中規定執行緒優先順序是1~10 的整數,較大的優先順序 能提高該執行緒被 CPU 調度的機率 |
public void interrupt() | 中斷這個執行緒,異常處理機制 | |
public static boolean interrupted() | 判斷當前執行緒是否被打斷,清除打斷標記 | |
public boolean isInterrupted() | 判斷當前執行緒是否被打斷,不清除打斷標記 | |
public final void join() | 等待這個執行緒結束 | |
public final void join(long millis) | 等待這個執行緒死亡millis毫秒,0意味著永遠等待 | |
public final native boolean isAlive() | 執行緒是否存活(還沒有運行完畢) | |
public final void setDaemon(boolean on) | 將此執行緒標記為守護執行緒或用戶執行緒 | |
public long getId() | 獲取執行緒長整型 的 id | id 唯一 |
public state getState() | 獲取執行緒狀態 | Java 中執行緒狀態是用 6 個 enum 表示,分別為: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
public boolean isInterrupted() | 判斷是否被打 斷 | 不會清除 打斷標記 |
start與run
我們首先來介紹執行緒的兩個相似的啟動方法:
// 首先我們採用start:start方法是啟動該執行緒,執行緒開始運行
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.start();
log.debug("do other things ...");
}
// 然後我們直接使用run方法:run方法是調用該執行緒的run方法,實際是main執行緒在運行t1執行緒的run方法
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
同時我們可以通過查看執行緒狀態來判斷start和run的區別:
// 這裡我們僅對start判斷
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
// 我們可以注意到main狀態從就緒狀態切換為Runnable
NEW
RUNNABLE
03:45:12.255 c.Test5 [t1] - running...
所以我們給出小結:
-
直接調用 run 是在主執行緒中執行了 run,沒有啟動新的執行緒
-
使用 start 是啟動新的執行緒,通過新的執行緒間接執行 run 中的程式碼
sleep 與 yield
我們首先給出sleep的相關解釋:
- 調用 sleep 會讓當前執行緒從 Running 進入 Timed Waiting 狀態(阻塞)
- 其它執行緒可以使用 interrupt 方法打斷正在睡眠的執行緒,這時 sleep 方法會拋出 InterruptedException
- 睡眠結束後的執行緒未必會立刻得到執行
- 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性 。其底層還是sleep方法
- 在循環訪問鎖的過程中,可以加入sleep讓執行緒阻塞時間,防止大量佔用cpu資源
我們再給出yield的相關解釋:
- 調用 yield 會讓當前執行緒從 Running 進入 Runnable 就緒狀態,然後調度執行其它執行緒;具體的實現依賴於作業系統的任務調度器
- 加入當前執行緒中只有這一個方法,那麼停止該方法執行後仍舊執行該方法
我們採用程式碼來進行展示:
/*sleep狀態轉換:我們運行下面程式碼後可以看到其t1狀態從就緒狀態到Runnable到Timed Waiting*/
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
Thread.sleep(2000);
System.out.println(t1.getState());
}
/*sleep打斷睡眠執行緒,拋出異常:注意當睡眠時拋出異常後該程式的打斷狀態為false*/
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();
}
// 輸出結果為:
03:47:18.141 c.Test7 [t1] - enter sleep...
03:47:19.132 c.Test7 [main] - interrupt...
03:47:19.132 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test7$1.run(Test7.java:14)
/*sleep採用TimeUnit的方法更具有程式碼可讀性*/
@Slf4j(topic = "c.Test8")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
log.debug("enter");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
// Thread.sleep(1000);
}
}
/*yield簡單測試:我們需要在Linux虛擬機中使用單核cpu來處理該程式碼,下面我們同時使用一個cpu處理兩個進程*/
@Slf4j(topic = "c.TestYield")
public class TestYield {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
Thread.yield();
System.out.println("---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
}
}
// 我們會發現task1的count數更多,因為輪到task2時,它會調用yield可能會導致當前執行緒停止從而去運行另一個執行緒
---->1 119199
---->2 101074
最後我們講解一個項目中常用的案例:
// 在沒有利用 cpu 來計算時,不要讓 while(true) 空轉浪費 cpu,這時可以使用 yield 或 sleep 來讓出 cpu 的使用權 給其他程式
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// - 可以用 wait 或 條件變數達到類似的效果
// - 不同的是,後兩種都需要加鎖,並且需要相應的喚醒操作,一般適用於要進行同步的場景
// - sleep 適用於無需鎖同步的場景
// wait實現
synchronized(鎖對象) {
while(條件不滿足) {
try {
鎖對象.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
}
// 條件變數實現
lock.lock();
try {
while(條件不滿足) {
try {
條件變數.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}
Priority
下面我們來介紹執行緒的優先順序設置:
- 首先優先順序的大小從1~10,默認為5
- 優先順序只是起到一定影響作用,不會對執行緒的執行順序起到絕對作用,只是優先順序越高其獲得CPU可能性越大
我們給出簡單程式碼示例:
// 我們同樣需要在單核CPU下進行
@Slf4j(topic = "c.TestYield")
public class TestYield {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
// 運行結果:我們會發現t2的執行次數明顯高於t1
---->1 283500
---->2 374389
join
首先我們需要了解join的作用:
- 用於等待直至所指執行緒結束為止
我們採用一個簡單的例子進行解釋:
// 下述例子我們希望列印執行緒t1的r的值,但是我們都知道main和t1執行緒都是同時運行的,並且t1等待了1s,所以這時的r是沒有賦值的,為0
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("開始");
Thread t1 = new Thread(() -> {
log.debug("開始");
sleep(1);
log.debug("結束");
r = 10;
});
t1.start();
log.debug("結果為:{}", r);
log.debug("結束");
}
// 但是我們可以選擇使用join方法來使主執行緒等待,知道t1執行緒結束後再去運行main執行緒
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("開始");
Thread t1 = new Thread(() -> {
log.debug("開始");
sleep(1);
log.debug("結束");
r = 10;
});
t1.start();
log.debug("結果為:{}", r);
t1.join();
log.debug("結果為:{}", r);
log.debug("結束");
}
// 注意我們如果採用sleep或許也可以實現相同的效果,但是很難準確確定其實際執行緒的結束時刻,所以正常情況下無法使用sleep
此外我們還需要講解join的其他幾個性質:
- 我們可以藉助join來實現執行緒之間的同步操作
- 當多個執行緒都採用join時,我們需要等所有執行緒都結束後繼續運行
- join是可以設置時效性的,當超過時則不再等待而是直接運行,若不超過就按照執行緒結束時間計算
我們通過幾個案例解釋上述性質:
// 藉助join完成執行緒之間的同步操作(其實前面第一個例子就是同步操作,我們需要先完成t1執行緒才能執行main執行緒的內容)
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("開始");
Thread t1 = new Thread(() -> {
log.debug("開始");
sleep(1);
log.debug("結束");
r = 10;
});
t1.start();
t1.join();
log.debug("結果為:{}", r);
log.debug("結束");
}
// 多個join需要等待所有執行緒完成
// 如下述,一個需要1s,一個需要2s,但由於同時進行,所以我們只需要2s就可以全部執行,然後再執行main
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
// join是可以設置時效性的,當超過時則不再等待而是直接運行,若不超過就按照執行緒結束時間計算
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
// 這裡設置時間,若低於1.5s按sleep時間執行;若高於1.5s則在1.5s時執行
sleep(1);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 執行緒執行結束會導致 join 結束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
interrupt
我們首先來簡單介紹一下interrupt方法:
- 翻譯過來就是打斷,本質是將執行緒的打斷標記設為true,並調用執行緒的三個parker對象(C++實現級別)unpark該執行緒。
我們來進行更詳細的介紹:
- 打斷操作實際上只是通知,不是打斷
- 打斷執行緒不等於中斷執行緒,有以下兩種情況:
- 打斷正在運行中的執行緒並不會影響執行緒的運行,但如果執行緒監測到了打斷標記為true,可以自行決定後續處理。
- 打斷阻塞中的執行緒會讓此執行緒產生一個InterruptedException異常,結束執行緒的運行。
- 但如果該異常被執行緒捕獲住,該執行緒依然可以自行決定後續處理(終止運行,繼續運行,做一些善後工作等等)
總而言之我們的打斷其實主要分為兩種類型,我們採用程式碼解釋:
/* 正常運行情況下打斷,並不會影響執行緒正常運行,但是會將執行緒的打斷標記設置為true */
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 這裡就是一個正常運行的執行緒,我們在啟動後1s對他進行打斷
while(true) {
// 打斷不會直接對執行緒造成影響,但是會將打斷狀態interrupted變為true(由isInterrupted方法得到)
boolean interrupted = Thread.currentThread().isInterrupted();
// 然後我們可以自行根據打斷狀態來做對應處理!
if(interrupted) {
log.debug("被打斷了, 退出循環");
break;
}
}
}, "t1");
t1.start();
// 1s後實現打斷操作
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
// 產生結果為:程式不停止,但打斷狀態為true
20:57:37.964 [t2] c.TestInterrupt - 打斷狀態: true
/* 不正常狀態被打斷,例如sleep,yield,join,會使執行緒進入阻塞狀態,拋出異常,會清空打斷狀態,使其變為false */
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
sleep(1);
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
log.debug(" 打斷狀態: {}", t1.isInterrupted());
}
// 產生結果為:拋出異常,但程式不會停止
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打斷狀態: false
最後我們介紹一個用於interrupt的思想模式之兩階段終止:
- 我們需要在執行緒1調用執行緒2的interrupt方法,同時使執行緒2完成它的後續操作
我們首先給出其邏輯圖:
我們通過程式碼解釋:
/*首先利用isinterrupt方法來實現思想*/
// 我們需要書寫一個檢測類負責不斷檢測打斷狀態
class TPTInterrupt {
private Thread thread;
public void start(){
thread = new Thread(() -> {
// 首先不斷檢測
while(true) {
Thread current = Thread.currentThread();
// 當我們發現被打斷時
if(current.isInterrupted()) {
// 自行處理後續內容
log.debug("料理後事");
break;
}
// 當我們發現沒有被打斷時,我們將程式停止一段時間,並進行簡單處理
try {
Thread.sleep(1000);
log.debug("將結果保存");
} catch (InterruptedException e) {
// 這裡我們需要注意,如果存在sleep等操作就會導致拋出異常InterruptedException
// 但是拋出異常並不會導致程式結束,也不會導致打斷標記為true,
// 所以我們需要手動設置打斷標記為true,使其在下一次循環時,中斷程式
current.interrupt();
}
// 執行監控操作
}
},"監控執行緒");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
// 我們在主程式調用:
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
// 我們可以得到結果:
11:49:42.915 c.TwoPhaseTermination [監控執行緒] - 將結果保存
11:49:43.919 c.TwoPhaseTermination [監控執行緒] - 將結果保存
11:49:44.919 c.TwoPhaseTermination [監控執行緒] - 將結果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [監控執行緒] - 料理後事
/*我們還可以手動設置一個參數來負責執行緒的關閉*/
// 停止標記用 volatile 是為了保證該變數在多個執行緒之間的可見性
// 我們的例子中,即主執行緒把它修改為 true 對 t1 執行緒可見
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
log.debug("料理後事");
break;
}
try {
Thread.sleep(1000);
log.debug("將結果保存");
} catch (InterruptedException e) {
}
// 執行監控操作
}
},"監控執行緒");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
// 我們在主程式中調用:
TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
// 我們得到下屬結果:
11:54:52.003 c.TPTVolatile [監控執行緒] - 將結果保存
11:54:53.006 c.TPTVolatile [監控執行緒] - 將結果保存
11:54:54.007 c.TPTVolatile [監控執行緒] - 將結果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [監控執行緒] - 料理後事
/*我們還可以通過打斷 park 執行緒來實現思想*/
// 首先我們簡單介紹一下park:當打斷標記為false時,park起到執行緒暫停作用;當打斷標記為true時,繼續運行
// 首先我們給出打斷標記為false的狀態
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打斷狀態:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
// 我們可以得到下述結果
21:11:52.795 [t1] c.TestInterrupt - park...
21:11:53.295 [t1] c.TestInterrupt - unpark...
21:11:53.295 [t1] c.TestInterrupt - 打斷狀態:true
// 如果打斷標記已經是 true, 則 park 會失效
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打斷狀態:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
// 我們可以得到下述結果:
21:13:48.783 [Thread-0] c.TestInterrupt - park...
21:13:49.809 [Thread-0] c.TestInterrupt - 打斷狀態:true
21:13:49.812 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打斷狀態:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打斷狀態:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打斷狀態:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打斷狀態:true
// 我們可以使用 Thread.interrupted() 清除打斷狀態
// Thread.interrupted()獲得當前打斷狀態,並將打斷狀態設置為false
// Thread.currentThread().isInterrupted())只能獲得當前打斷狀態,不會影響值
/*
此外還有一些不符合當前狀態的打斷方式,我們也簡單介紹一下:
- 使用執行緒對象的 stop() 方法停止執行緒
- stop 方法會真正殺死執行緒,如果這時執行緒鎖住了共享資源,那麼當它被殺死後就再也沒有機會釋放鎖, 其它執行緒將永遠無法獲取鎖
- 使用 System.exit(int) 方法停止執行緒
- 目的僅是停止一個執行緒,但這種做法會讓整個程式都停止
*/
過時方法介紹
最後我們給出三個執行緒的過時方法簡單解釋:
法名 | static | 功能說明 |
---|---|---|
stop() | 停止執行緒運行 | |
suspend() | 掛起(暫停)執行緒運行 | |
resume() | 恢復執行緒運行 |
主執行緒和守護執行緒
首先我們簡單介紹一下守護執行緒:
- 守護執行緒是不重要的執行緒,當所有的主執行緒都完成時,無論守護執行緒是否結束都被迫結束
我們給出簡單示例:
// 主函數程式碼:
log.debug("開始運行...");
Thread t1 = new Thread(() -> {
log.debug("開始運行...");
sleep(2);
log.debug("運行結束...");
}, "daemon");
// 設置該執行緒為守護執行緒
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("運行結束...");
// 運行結果:
08:26:38.123 [main] c.TestDaemon - 開始運行...
08:26:38.213 [daemon] c.TestDaemon - 開始運行...
08:26:39.215 [main] c.TestDaemon - 運行結束...
我們可以簡單給出守護執行緒的一些實例:
- 垃圾回收器執行緒就是一種守護執行緒
- Tomcat 中的 Acceptor 和 Poller 執行緒都是守護執行緒,所以 Tomcat 接收到 shutdown 命令後,不會等待它們處理完當前請求
執行緒狀態
這一小節我們將介紹執行緒的兩種狀態形式
執行緒五種狀態
從作業系統的角度來講,執行緒具有五種狀態:
我們來簡單介紹一下:
- 【初始狀態】僅是在語言層面創建了執行緒對象,還未與作業系統執行緒關聯
- 【可運行狀態】(就緒狀態)指該執行緒已經被創建(與作業系統執行緒關聯),可以由 CPU 調度執行
- 【運行狀態】指獲取了 CPU 時間片運行中的狀態
- 當 CPU 時間片用完,會從【運行狀態】轉換至【可運行狀態】,會導致執行緒的上下文切換
- 【阻塞狀態】
- 如果調用了阻塞 API,如 BIO 讀寫文件,這時該執行緒實際不會用到 CPU,會導致執行緒上下文切換,進入 【阻塞狀態】
- 等 BIO 操作完畢,會由作業系統喚醒阻塞的執行緒,轉換至【可運行狀態】
- 與【可運行狀態】的區別是,對【阻塞狀態】的執行緒來說只要它們一直不喚醒,調度器就一直不會考慮 調度它們
- 【終止狀態】表示執行緒已經執行完畢,生命周期已經結束,不會再轉換為其它狀態
執行緒六種狀態
從Java虛擬機的角度來看,將其分為六種狀態:
我們來簡單介紹一下:
- NEW 執行緒剛被創建,但是還沒有調用 start() 方法
- RUNNABLE 當調用了 start() 方法之後,注意,Java API 層面的 RUNNABLE 狀態涵蓋了 作業系統 層面的 【可運行狀態】、【運行狀態】和【阻塞狀態】(由於 BIO 導致的執行緒阻塞,在 Java 里無法區分,仍然認為 是可運行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 層面對【阻塞狀態】的細分,後面會在狀態轉換一節 詳述
- TERMINATED 當執行緒程式碼運行結束
我們給出相關程式碼進行解釋:
package cn.itcast.n3;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
// 程式碼完善,但並未運行,屬於NEW狀態
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
// 程式碼完善且一直運行,屬於runnable狀態
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable
}
}
};
t2.start();
// 執行一次結束,屬於執行緒執行完畢,TERMINATED 狀態
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
// 由sleep停止進程,屬於timed_waiting狀態
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
// 由於等待其他進程結束而等待,屬於waiting狀態
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
// 由於等待鎖而等待,屬於blocked狀態
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 輸出其狀態
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
本章小結
我們簡單總結一下重點:
- 執行緒創建
- 執行緒重要 api,如 start,run,sleep,join,interrupt 等
- 執行緒狀態
- 應用方面
- 非同步調用:主執行緒執行期間,其它執行緒非同步執行耗時操作
- 提高效率:並行計算,縮短運算時間
- 同步等待:join
- 統籌規劃:合理使用執行緒,得到最優效果
- 原理方面
- 執行緒運行流程:棧、棧幀、上下文切換、程式計數器
- Thread 兩種創建方式 的源碼
- 模式方面
- 終止模式之兩階段終止
結束語
到這裡我們JUC的進程與執行緒內容就結束了,希望能為你帶來幫助~
附錄
該文章屬於學習內容,具體參考B站黑馬程式設計師滿老師的JUC完整教程
這裡附上影片鏈接:01.001-為什麼學習並發_嗶哩嗶哩_bilibili