## 進程與線程的區別 - 進程基本上相互獨立的,而線程存在於進程內,是進程的一個子集 - 進程擁有共用的資源,如記憶體空間等,供其內部的線程共用 - 進程間通信較為複雜 - 同一臺電腦的進程通信稱為 IPC(Inter-process communication) - 不同電腦之間的進程... ...
進程與線程
進程
- 程式由指令和數據組成,但這些指令要運行,數據要讀寫,就必須將指令載入至 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() | 恢複線程運行 |