章節目錄
1. 進程線程
1.1 進程
程序由指令和數據組成。但這些指令要運行,數據要讀寫,就必須將指令加載至CPU,數據加載至內存。在指令運行過程中還需要用到磁盤、網絡等設備,而進程就是用來加載指令、管理內存、管理IO的
程序和進程
當一個程序被運行,從磁盤加載這個程序的代碼至內存。這時,就開啓了一個進程。進程(動態)可以視爲程序(靜態)的一個實例。大部分程序可以同時運行多個實例進程。如:記事本、瀏覽器;也有的程序只能啓動一個實例進程。如:任務管理器
1.2 線程
一個進程可以分爲多個線程,而一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給CPU執行。
在Java中,線程作爲最小調度單位,進程作爲資源分配的最小單位。
2. Java中的線程
在Java程序啓動時,都會創建一個線程----Main線程。
2.1 創建並運行線程
如果你想在Main線程之外創建其他線程,可以用如下方法。
- Thread
- Runnable接口
- FutureTask
2.1.1 Thread
// 創建線程 (使用了匿名內部類)
Thread t1 = new Thread() {
public void run() {
// 要執行的任務
}
}
// 啓動線程
t1.start()
2.1.2 Runnable
Runnable r = new Runnable() {
public void run() {
// 要執行的任務
}
}
// 創建線程對象
Thread t1 = new Thread(r, "t1");
// 啓動線程
t1.start();
Java8 以後可以使用 Lambda 表達式來精簡代碼。如:
Runnable r = () -> {
// 要執行的任務
}
// 創建線程對象
Thread t1 = new Thread(r, "t1");
// 啓動線程
t1.start();
如果對 Lambda 表達式不瞭解的,可以看看這篇博客 手把手地帶你走進Lambda表達式之門
2.1.3 FutureTask
FutureTask 能夠接收 Callable 接口的參數,用來處理有返回結果的情況。
FutureTask<Integer> task = new FutureTask<>(() -> {
// 要執行的任務
return 666;
})
new Thread(task, "t1").start();
// 主線程阻塞,同步等待 task 執行完畢的結果
Integer result = task.get();
2.1.4 Thread與Runnable
查看 Thread 源碼:
- Thread 實現了 Runnable 接口
- Thread 有一個 Runnable 類型的成員變量 target
查看 Thread 的構造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
最終會調用:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
this.target = target;
...
}
將構造方法中的參數傳遞給了成員變量 target。
查看 Thread 中的 run() 方法:
public void run() {
if (target != null) {
target.run();
}
}
如果 target != null,則運行 Runnable 接口中的 run() 方法;否則,直接運行Thread 中的 run() 方法。
方法1 中的原理:
通過 匿名內部類實現的方式可以看做是 Thread 的子類(亦或繼承 Thread),子類重寫了父類中的 run() 方法。所以,最終運行的是子類中的方法
方法2 中的原理:
把 Runnable 類型的變量作爲 Thread 的構造方法的參數,在此構造方法中,將此參數傳遞給成員變量 target,所以 target != null,最終會執行 Runnable 接口中的方法,而上述的 Lambda 表達式將 Runnable 接口中的 run() 方法進行重寫,所以,最終會執行 Lambda表達式。
2.2 棧與棧幀
JVM是由堆、棧、方法區等組成,其中,棧內存是給線程用的,每個線程啓動後,JVM會爲它分配一個棧內存。而每個棧是由多個棧幀組成,對應着每次方法調用時所佔用的內存
2.3 上下文切換
因爲以下原因導致CPU不再執行當前的線程,轉而執行另一個線程的代碼:
- 線程的CPU時間片用完
- 垃圾回收
- 有更優先級的線程需要執行
- 線程自己調用了sleep()、wait()、lock() 方法等
3. 常用的方法
方法名 | 功能描述 | 注意 |
---|---|---|
start | 啓動一個新的線程,在新的線程中運行 run() 方法 | start() 方法只是讓線程進入就緒狀態,裏面的代碼不一定就馬上執行(CPU時間片還沒分給它)。每個線程對象的 start() 方法只能調用一次,如果調用多次,會拋出異常IllegalThreadStateException |
run | 新的線程啓動後,會執行的代碼 | |
join | 等待線程運行結束 | |
isInterrupted | 判斷是否被打斷 | 不會清除打斷標記 |
interrupt | 打斷線程 | 如果被打斷的線程正在sleep、wait、join 會導致被打斷的線程拋出異常 InterruptedException,並清除標記;如果打斷正在運行的線程,則會設置打斷標記;park 線程被打斷,也會設置打斷標記 |
interrupted | 判斷當前線程是否被打斷 | 會清除打斷標記 |
sleep(n) | 讓當前執行的線程休眠 n 毫秒,休眠時間讓出 CPU | |
yield | 提示線程調度器讓出當前線程對 CPU 的使用 |
3.1 start()與run()
start() 方法表示啓動一個新的線程,run() 方法表示線程啓動後要執行的代碼,那能否能直接調用 run() 方法?
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run () {
System.out.println("hello world" + "=====" +Thread.currentThread().getName());
}
};
t1.run();
}
run() 方法還是可以執行,但並沒有創建新的線程,還是在 main 線程中執行的
3.2 sleep()
- 調用 sleep() 方法會讓當前線程從 Running 進入 Timed Waiting 狀態
- 其它線程可以使用 interrupt() 方法打斷正在睡眠的線程,這時,sleep() 方法會拋出 InterruptedException異常
public static void main(String[] args) throws Exception{
Thread t1 = new Thread() {
@Override
public void run () {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("wake up");
}
}
};
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
3.3 join()
看下這段代碼,打印 i 是什麼?
static int i = 1;
private static void test() throws Exception{
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
i = 100;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
System.out.println("i = " + i);
}
public static void main(String[] args) throws Exception{
test();
}
流程分析:main 線程啓動,調用 test() 方法,開啓一個新的線程 t1,main 和 t1 線程都是在同時運行的,main 線程會先執行語句 “System.out.println("i = " + i);”,只有當 t1 線程休眠1s後,纔會將 i 賦值爲 100。
那麼,如何讓打印的 i 爲100呢?
只需要添加一個 join() 方法
t1.start();
t1.join();
System.out.println("i = " + i);
當 main 線程 執行到語句 “t1.join()”時,main 線程會一直等待着 t1 線程(t1 調用了 join()方法),直到 t1 線程運行結束後,main 線程纔會繼續執行。
3.4 interrupt()
3.4.1 打斷阻塞的線程
打斷 sleep、wait、join 的線程會清空打斷標記。以 sleep 爲例
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(100);
t1.interrupt();
System.out.println("打斷標記:" + t1.isInterrupted());
}
3.4.2 打斷運行的線程
打斷運行的線程是不會清除標記的
3.4.3 應用
兩階段終止模式
在線程 t1 中如何優雅地終止線程 t2?
先看看兩階段終止模式的一個應用場景----做一個系統的健康監控,如:定時地去監控CPU 的使用率、內存的使用率等。可以使用一個後臺的監控線程每隔2s不斷地進行記錄即可,當點擊停止按鈕時便不再監控了。
代碼實現:
public class TwoPhraseTermination {
// 監控線程
private Thread monitor;
// 啓動監控線程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
System.out.println("料理後事");
break;
}
try {
Thread.sleep(2000);
System.out.println("執行監控記錄");
} catch (InterruptedException e) {
// 重新設置打斷標記
current.interrupt();
}
}
});
monitor.start();
}
public void stop() {
monitor.interrupt();
}
public static void main(String[] args) throws Exception{
TwoPhraseTermination termination = new TwoPhraseTermination();
termination.start();
Thread.sleep(5000);
termination.stop();
}
}
這就是實現了在一個 main 線程中優雅地終止了另一個線程。