併發編程之線程第一篇 3.4 原理之線程運行 線程上下文切換(Thread Context Switch) 3.5 常見方法 3.6 start與run 3.7 sleep與yield 案例 - 防止CPU占用100% 3.8 join方法詳解 3.9 interrupt方法詳解 兩階段終止模式 3 ...
併發編程之線程第一篇
3.4 原理之線程運行
Java虛擬機棧
JVM中由堆、棧、方法區所組成,其中棧記憶體是給線程使用,每個線程啟動後,虛擬機就會為其分配一塊棧記憶體。
- 每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所占用的記憶體
- 每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法
線程上下文切換(Thread Context Switch)
因為以下一些原因導致cpu不再執行當前的線程,轉而執行另一個線程的代碼
- 線程的cpu時間片用完
- 垃圾回收
- 有更高優先順序的線程需要運行
- 線程自己調用了sleep、yield、join、park、synchronized、lock等方法程式
當Context Switch發生時,需要由操作系統保存當前線程的狀態,並恢復另一個線程的狀態,Java中對應的概念就是程式計數器(Program Counter Register),它的作用是記住下一條jvm指令的執行地址,是線程私有的
3.5 常見方法
方法名 | 功能說明 | 註意 |
---|---|---|
start() | 啟動一個新線程,在新的線程運行run方法中的代碼 | start方法只是讓線程進入就緒,裡面的代碼不一定立刻運行(CPU的時間片還沒分給它)。每個線程對象的start方法只能調用一次,如果調用了多次會出現IllegalThreadStateException |
run() | 新線程啟動後會調用的方法 | 如果在構造Thread對象時傳遞了Runnable參數,則線程啟動後調用Runnable中的run 方法,否則預設不執行任何操作。但可以創建Thread的子類對象,來覆蓋預設行為 |
join() | 等待線程運行結束 | |
join(long n) | 等待線程運行結束,最多等待n毫秒 | |
getId() | 獲取線程長整型的id | id唯一 |
getName() | 獲取線程名 | |
setName(String) | 修改線程名 | |
getPriority() | 獲取線程優先順序 | |
setPriority(int) | 修改線程優先順序 | java中規定線程優先順序是1~10的整數,較大的優先順序能提高該線程被CPU調度的機率 |
getState() | 獲取線程狀態 | java中線程狀態是用6個enum表示,分別為 :NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED |
isInterrupted() | 判定是否被打斷 | 不會清除打斷標記 |
isAlive() | 線程是否存活(還沒有運行完畢) | |
interrupt() | 打斷線程 | 如果被打斷線程正在sleep,wait,join會導致被打斷的線程拋出InterruptedException,並清除打斷標記;如果打斷的正在運行的線程,則會設置打斷標記;park的線程被打斷,也會設置打斷標記 |
interrupted() static | 判定當前線程是否被打斷 | 會清除打斷標記 |
currendThread(0 static | 獲取當前正在執行的線程 | |
sleep(long n) static | 讓當前執行的線程休眠n毫秒,休眠時讓出cpu的時間片給其他線程 | |
yieId() static | 提示線程調度器讓出當前線程對CPU的使用 | 主要是為了測試和調試 |
3.6 start與run
調用run
輸出
程式仍在main線程運行,FileReader.read()方法調用還是同步的
3.7 sleep與yield
sleep
- 調用sleep會讓當前線程從Running進入Timed Waiting狀態
- 其它線程可以使用 interrupt方法打斷正在睡眠的線程,這時sleep方法會拋出InterruptedException
- 睡眠結束後的線程未必會立刻得到執行
- 建議用TimeUnit的sleep代替Thread的sleep來獲得更好地可讀性
yield
1、 調用yield會讓當前線程從Running進入Runnable狀態,然後調度執行其它同優先順序的線程。如果這時沒有同優先順序的線程,那麼不能保證讓當前線程暫停的效果
2、具體的實現依賴於操作系統的任務調度器
線程優先順序
- 線程優先順序會提示(hint)調度器優先調度該線程,但它僅僅是一個提示,調度器可以忽略它
- 如果cpu比較忙,那麼優先順序高的線程會獲得更多的時間片,但cpu閑時,優先順序幾乎沒作用
案例 - 防止CPU占用100%
sleep實現
在沒有利用cpu來計算時,不要讓while(true)空轉浪費cpu,這時可以使用yield或sleep來讓出cpu的使用權給其他程式
- 可以用wait或條件變數達到類似的效果
- 不同的是,後兩種都需要加鎖,並且需要相應的喚醒操作,一般適用於要進行同步的場景
- sleep適用於無需鎖同步的場景
3.8 join方法詳解
為什麼需要join
下麵的代碼執行,列印r是什麼?
分析
- 因為主線程和線程t1是並行執行的,t1線程需要1秒之後才能算出r=10
- 而主線程一開始就要列印r的結果,所以只能列印出r=0
解決方法 - 用sleep行不行?為什麼?
- 用join,加在start之後即可
應用之同步 (案例1)
以調用方角度來講,如果 - 需要等待結果返回,才能繼續運行就是同步
- 不需要等待結果返回,就能繼續運行就是非同步
有時效的join
等夠時間
輸出
3.9 interrupt方法詳解
打斷sleep,wait,join的線程
打斷sleep的線程,會清空打斷狀態,以sleep為例
輸出
打斷正常運行的線程
打斷正常運行的線程,不會清空打斷狀態
輸出
兩階段終止模式
Two Phase Termination
在一個線程T1中如何“優雅”終止線程T2?這裡的【優雅】指的是給T2一個料理後事的機會。
1、 錯誤思路
- 使用線程對象的stop()停止線程
(1)stop方法會真正殺死線程,如果這時線程鎖住了共用資源,那麼當它被殺死後就再也沒有機會釋放鎖,其它線程將永遠無法獲取鎖。 - 使用System.exit(int)方法停止線程
(1)目的僅是停止一個線程,但這種做法會讓整個程式都停止
package com.example.demo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TwoPhaseTermination {
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
test.start();
Thread.sleep(3500);
test.stop();
}
}
@Slf4j
class Test{
private Thread monitor;
/**
* 啟動監控線程
*/
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()) {
log.info("料理後事");
break;
}
try {
// 情況1
Thread.sleep(1000);
log.info("執行監控記錄");
} catch (Exception e) {
// 因為sleep出現異常後,會消除打斷標記
// 需要重置打斷標記
e.printStackTrace();
currentThread.interrupt();
}
}
});
monitor.start();
}
/**
* 停止監控線程
*/
public void stop() {
monitor.interrupt();
}
}
打斷park線程
打斷park線程,不會清空打斷狀態
輸出
如果打斷標記已經是true,則park會失效,可以如下操作 :
3.10 不推薦的方法
還有一些不推薦使用的方法,這些方法已過時,容易破壞同步代碼塊,造成線程死鎖。
方法名 | static | 功能說明 |
---|---|---|
stop() | 停止線程運行 | |
suspend() | 掛起(暫停)線程運行 | |
resume() | 恢複線程運行 |
3.11 主線程與守護線程
預設情況下,Java進程需要等待所有線程都運行結束,才會結束。有一種特殊的線程叫做守護線程,只要其它非守護線程運行結束了,即使守護線程的代碼沒有執行完,也會強制結束。
註意
- 垃圾回收器線程就是一種守護線程
- Tomcat中的Acceptor和Poller線程都是守護線程,所以Tomcat接收到shutdown命令後,不會等待它們處理完當前請求。