程式與執行緒
程式
- 程式由指令和資料組成,但這些指令要執行,資料要讀寫,就必須將指令載入至 CPU,資料載入至記憶體。在指令執行過程中還需要用到磁碟、網路等裝置。程式就是用來載入指令、管理記憶體、管理 IO 的
- 當一個程式被執行,從磁碟載入這個程式的程式碼至記憶體,這時就開啟了一個程式
執行緒
- 一個程式之內可以分為一到多個執行緒。
- 一個執行緒就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執行
- Java 中,執行緒作為最小排程單位,程式作為資源分配的最小單位。 在 windows 中程式是不活動的,只是作為執行緒的容器
程式與執行緒的區別
- 程式基本上相互獨立的,而執行緒存在於程式內,是程式的一個子集
- 程式擁有共享的資源,如記憶體空間等,供其內部的執行緒共享
- 程式間通訊較為複雜
- 同一臺計算機的程式通訊稱為 IPC(Inter-process communication)
- 不同計算機之間的程式通訊,需要通過網路,並遵守共同的協議,例如 HTTP
- 執行緒通訊相對簡單,因為它們共享程式內的記憶體,一個例子是多個執行緒可以訪問同一個共享變數
- 執行緒更輕量,執行緒上下文切換成本一般上要比程式上下文切換低
並行與併發
單核 cpu 下,執行緒實際還是 序列執行 的。作業系統中有一個元件叫做任務排程器,將 cpu 的時間片(windows下時間片最小約為 15 毫秒)分給不同的程式使用,只是由於 cpu 線上程間(時間片很短)的切換非常快,人類感覺是 同時執行的 。總結為一句話就是: 微觀序列,巨集觀並行 。一般會將這種 執行緒輪流使用 CPU 的做法稱為併發 (concurrent)
多核 cpu下,每個 核(core) 都可以排程執行執行緒,這時候執行緒可以是並行的。
Java 執行緒
建立和執行執行緒
-
直接使用 Thread
package create; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.ThreadCre") public class ThreadCre { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { log.debug("running"); } }; t.start(); log.debug("running"); } }
-
使用 Runnable 配合 Thread
package create; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.RunnableCre") public class RunnableCre { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { log.debug("running"); } }; Thread t = new Thread(r,"t2"); t.start(); } }
使用 lambda 方式簡化
package create; import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.RunnableCre") public class RunnableCre { public static void main(String[] args) { Runnable r = () -> { log.debug("running"); }; Thread t = new Thread(r,"t2"); t.start(); } }
-
FutureTask 配合 Thread
package create; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; @Slf4j(topic = "c.FutureTaskCre") public class FutureTaskCre { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() { @Override public Integer call() throws Exception { log.debug("running..."); Thread.sleep(1000); return 100; } }); Thread t = new Thread(task,"t1"); t.start(); log.debug("{}",task.get()); } }
Thread 與 Runnable 的關係
- 用 Runnable 更容易與執行緒池等高階 API 配合
- 用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活
執行緒執行的原理
棧與棧幀
每個執行緒啟動後,虛擬機器就會為其分配一塊棧記憶體。
- 每個棧由多個棧幀(Frame)組成,對應著每次方法呼叫時所佔用的記憶體
- 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法
執行緒上下文切換
因為以下一些原因導致 cpu 不再執行當前的執行緒,轉而執行另一個執行緒的程式碼
- 執行緒的 cpu 時間片用完
- 垃圾回收
- 有更高優先順序的執行緒需要執行
- 執行緒自己呼叫了 sleep、yield、wait、join、park、synchronized、lock 等方法
當 Context Switch 發生時,需要由作業系統儲存當前執行緒的狀態,並恢復另一個執行緒的狀態,Java 中對應的概念就是程式計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是執行緒私有的
- 狀態包括程式計數器、虛擬機器棧中每個棧幀的資訊,如區域性變數、運算元棧、返回地址等
- Context Switch 頻繁發生會影響效能
常見方法
方法名 | static | 功能說明 | 注意 |
---|---|---|---|
start() | 啟動一個新執行緒,在新的執行緒執行 run 方法中的程式碼 | start 方法只是讓執行緒進入就緒,裡面的程式碼不一定立刻執行(CPU的時間片還沒有分給它)。每個執行緒物件的 start 方法只能呼叫一次,否則會出現異常 | |
run() | 新執行緒啟動後會呼叫的方法 | 如果在構造 Thread 物件時傳遞了 Runnable 引數,則執行緒啟動後會呼叫 Runnable 中的 run 方法。但可以建立 Thread 的子類物件來覆蓋預設行為 | |
join() | 等待執行緒執行結束 | ||
join(long n) | 等待執行緒執行結果,最多等待 n 毫秒 | ||
getId() | 獲取執行緒長整型的 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 | 判斷當前執行緒是否被打斷 | 會清除 打斷標記 |
currentThread() | static | 獲取當前正在執行的執行緒 | |
sleep(long n) | static | 讓當前執行的執行緒休眠 n 毫秒,休眠時讓出 CPU 的時間片給其他程式 | |
yield() | static | 提示執行緒排程器讓出當前執行緒對CPU的使用 | 主要是為了測試和除錯 |
start 與 run
呼叫 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 ...");
}
輸出
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
程式仍在 main 執行緒執行, FileReader.read() 方法呼叫還是同步的
總結
- 直接呼叫 run 是在主執行緒中執行了 run,沒有啟動新的執行緒
- 使用 start 是啟動新的執行緒,通過新的執行緒間接執行 run 中的程式碼
sleep 與 yield
sleep
- 呼叫 sleep 會讓當前執行緒從 Running 進入 Timed Waiting 狀態(阻塞)
- 其它執行緒可以使用 interrupt 方法打斷正在睡眠的執行緒,這時 sleep 方法會丟擲 InterruptedException
- 睡眠結束後的執行緒未必會立刻得到執行(搶佔時間片)
- 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性
yield
- 呼叫 yield 會讓當前執行緒從 Running 進入 Runnable 就緒狀態,然後排程執行其它執行緒
- 具體的實現依賴於作業系統的任務排程器
執行緒優先順序
- 執行緒優先順序會提示(hint)排程器優先排程該執行緒,但它僅僅是一個提示,排程器可以忽略它
- 如果 cpu 比較忙,那麼優先順序高的執行緒會獲得更多的時間片,但 cpu 閒時,優先順序幾乎沒作用
join
等待一個執行緒執行結束
等待多個執行緒的結果
情況一:
package testJoin;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.demo1")
public class demo1 {
static int r = 0 , r1 = 0 , r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
r1 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);
}
}
輸出:
14:18:02 [main] c.demo1 - join begin
14:18:03 [main] c.demo1 - t1 join end
14:18:04 [main] c.demo1 - t2 join end
14:18:04 [main] c.demo1 - r1: 20 r2: 0 cost: 2008
情況二:
package testJoin;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.demo1")
public class demo1 {
static int r = 0 , r1 = 0 , r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
r1 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);
}
}
輸出:
14:19:19 [main] c.demo1 - join begin
14:19:21 [main] c.demo1 - t2 join end
14:19:21 [main] c.demo1 - t1 join end
14:19:21 [main] c.demo1 - r1: 20 r2: 0 cost: 2006
另外 join 也可以帶引數,是有時效的等待。當到設定時間執行緒還未給出結果,直接向下執行,不再等待。如果設定時間還沒到但是執行緒已經執行完畢,則直接向下執行,不再等待。
interrupt
打斷 sleep,wait,join 的執行緒
這幾個方法都會讓執行緒進入阻塞狀態
打斷 sleep 的執行緒, 會清空打斷狀態,以 sleep 為例
package testInterrupt;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.demo1")
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000);
//注意:sleep,wait,join等被打斷並以異常形式表現出來後
// 會把打斷標記重新置為 false(未打斷狀態)
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打斷標記:{}",t1.isInterrupted());
}
}
輸出:
15:08:12 [t1] c.demo1 - sleep...
15:08:13 [main] c.demo1 - interrupt
15:08:13 [main] c.demo1 - 打斷標記:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at testInterrupt.demo1.lambda$main$0(demo1.java:11)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
打斷正常執行的執行緒打斷標記置為:true
package testInterrupt;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.demo2")
public class demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true){
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted){
log.debug("被打斷了,退出迴圈");
break;
}
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
輸出:
15:17:40 [main] c.demo2 - interrupt
15:17:40 [t1] c.demo2 - 被打斷了,退出迴圈
打斷 park 執行緒
package testInterrupt;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.demo4")
public class demo4 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打斷狀態:{}",Thread.currentThread().isInterrupted());
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
輸出:
14:16:21 [t1] c.demo4 - park...
14:16:22 [t1] c.demo4 - unpark...
14:16:22 [t1] c.demo4 - 打斷狀態:true
兩階段終止模式
package testInterrupt;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.demo3")
public class demo3 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
private Thread monitor;
//啟動監控執行緒
public void start(){
monitor = new Thread(() -> {
while (true){
Thread current = Thread.currentThread();
if(current.isInterrupted()){
log.debug("料理後事");
break;
}
try {
Thread.sleep(1000);//情況1
log.debug("執行監控記錄");//情況2
} catch (InterruptedException e) {
e.printStackTrace();
//重新設定打斷標記
current.interrupt();
}
}
});
monitor.start();
}
//終止監控執行緒
public void stop(){
monitor.interrupt();
}
}
輸出:
15:33:02 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
15:33:03 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
15:33:04 [Thread-0] c.TwoPhaseTermination - 執行監控記錄
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at testInterrupt.TwoPhaseTermination.lambda$start$0(demo3.java:29)
at java.lang.Thread.run(Thread.java:748)
15:33:04 [Thread-0] c.TwoPhaseTermination - 料理後事
Process finished with exit code 0
不推薦的方法
還有一些不推薦使用的方法,這些方法已過時,容易破壞同步程式碼塊,造成執行緒死鎖
方法名 | static | 功能說明 |
---|---|---|
stop() | 停止執行緒執行 | |
suspend() | 掛起(暫停)執行緒執行 | |
resume() | 恢復執行緒執行 |