Java 線程基礎
📦 本文以及示例源碼已歸檔在 javacore
一、線程簡介
什么是進程
簡言之,進程可視為一個正在運行的程序。它是系統運行程序的基本單位,因此進程是動態的。進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動。進程是操作系統進行資源分配的基本單位。
什么是線程
線程是操作系統進行調度的基本單位。線程也叫輕量級進程(Light Weight Process),在一個進程里可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。
進程和線程的區別
- 一個程序至少有一個進程,一個進程至少有一個線程。
- 線程比進程划分更細,所以執行開銷更小,並發性更高。
- 進程是一個實體,擁有獨立的資源;而同一個進程中的多個線程共享進程的資源。
二、線程基本用法
線程(Thread
)基本方法清單:
方法 | 描述 |
---|---|
run |
線程的執行實體。 |
start |
線程的啟動方法。 |
currentThread |
返回對當前正在執行的線程對象的引用。 |
setName |
設置線程名稱。 |
getName |
獲取線程名稱。 |
setPriority |
設置線程優先級。Java 中的線程優先級的范圍是 [1,10],一般來說,高優先級的線程在運行時會具有優先權。可以通過 thread.setPriority(Thread.MAX_PRIORITY) 的方式設置,默認優先級為 5。 |
getPriority |
獲取線程優先級。 |
setDaemon |
設置線程為守護線程。 |
isDaemon |
判斷線程是否為守護線程。 |
isAlive |
判斷線程是否啟動。 |
interrupt |
中斷另一個線程的運行狀態。 |
interrupted |
測試當前線程是否已被中斷。通過此方法可以清除線程的中斷狀態。換句話說,如果要連續調用此方法兩次,則第二次調用將返回 false(除非當前線程在第一次調用清除其中斷狀態之后且在第二次調用檢查其狀態之前再次中斷)。 |
join |
可以使一個線程強制運行,線程強制運行期間,其他線程無法運行,必須等待此線程完成之后才可以繼續執行。 |
Thread.sleep |
靜態方法。將當前正在執行的線程休眠。 |
Thread.yield |
靜態方法。將當前正在執行的線程暫停,讓其他線程執行。 |
創建線程
創建線程有三種方式:
- 繼承
Thread
類 - 實現
Runnable
接口 - 實現
Callable
接口
繼承 Thread 類
通過繼承 Thread
類創建線程的步驟:
- 定義
Thread
類的子類,並覆寫該類的run
方法。run
方法的方法體就代表了線程要完成的任務,因此把run
方法稱為執行體。 - 創建
Thread
子類的實例,即創建了線程對象。 - 調用線程對象的
start
方法來啟動該線程。
public class ThreadDemo {
public static void main(String[] args) {
// 實例化對象
MyThread tA = new MyThread("Thread 線程-A");
MyThread tB = new MyThread("Thread 線程-B");
// 調用線程主體
tA.start();
tB.start();
}
static class MyThread extends Thread {
private int ticket = 5;
MyThread(String name) {
super(name);
}
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票");
ticket--;
}
}
}
}
實現 Runnable 接口
實現 Runnable
接口優於繼承 Thread
類,因為:
- Java 不支持多重繼承,所有的類都只允許繼承一個父類,但可以實現多個接口。如果繼承了
Thread
類就無法繼承其它類,這不利於擴展。 - 類可能只要求可執行就行,繼承整個
Thread
類開銷過大。
通過實現 Runnable
接口創建線程的步驟:
- 定義
Runnable
接口的實現類,並覆寫該接口的run
方法。該run
方法的方法體同樣是該線程的線程執行體。 - 創建
Runnable
實現類的實例,並以此實例作為Thread
的 target 來創建Thread
對象,該Thread
對象才是真正的線程對象。 - 調用線程對象的
start
方法來啟動該線程。
public class RunnableDemo {
public static void main(String[] args) {
// 實例化對象
Thread tA = new Thread(new MyThread(), "Runnable 線程-A");
Thread tB = new Thread(new MyThread(), "Runnable 線程-B");
// 調用線程主體
tA.start();
tB.start();
}
static class MyThread implements Runnable {
private int ticket = 5;
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票");
ticket--;
}
}
}
}
實現 Callable 接口
繼承 Thread 類 和 實現 Runnable 接口這兩種創建線程的方式都沒有返回值。所以,線程執行完后,無法得到執行結果。但如果期望得到執行結果該怎么做?
為了解決這個問題,Java 1.5 后,提供了
Callable
接口和Future
接口,通過它們,可以在線程執行結束后,返回執行結果。
通過實現 Callable
接口創建線程的步驟:
- 創建
Callable
接口的實現類,並實現call
方法。該call
方法將作為線程執行體,並且有返回值。 - 創建
Callable
實現類的實例,使用FutureTask
類來包裝Callable
對象,該FutureTask
對象封裝了該Callable
對象的call
方法的返回值。 - 使用
FutureTask
對象作為Thread
對象的 target 創建並啟動新線程。 - 調用
FutureTask
對象的get
方法來獲得線程執行結束后的返回值。
public class CallableDemo {
public static void main(String[] args) {
Callable<Long> callable = new MyThread();
FutureTask<Long> future = new FutureTask<>(callable);
new Thread(future, "Callable 線程").start();
try {
System.out.println("任務耗時:" + (future.get() / 1000000) + "毫秒");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
static class MyThread implements Callable<Long> {
private int ticket = 10000;
@Override
public Long call() {
long begin = System.nanoTime();
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票");
ticket--;
}
long end = System.nanoTime();
return (end - begin);
}
}
}
FAQ
start
和 run
方法有什么區別
run
方法是線程的執行體。start
方法會啟動線程,然后 JVM 會讓這個線程去執行run
方法。
可以直接調用 Thread
類的 run
方法么
- 可以。但是如果直接調用
Thread
的run
方法,它的行為就會和普通的方法一樣。 - 為了在新的線程中執行我們的代碼,必須使用
Thread
的start
方法。
線程休眠
使用 Thread.sleep
方法可以使得當前正在執行的線程進入休眠狀態。
使用 Thread.sleep
需要向其傳入一個整數值,這個值表示線程將要休眠的毫秒數。
Thread.sleep
方法可能會拋出 InterruptedException
,因為異常不能跨線程傳播回 main
中,因此必須在本地進行處理。線程中拋出的其它異常也同樣需要在本地進行處理。
public class ThreadSleepDemo {
public static void main(String[] args) {
new Thread(new MyThread("線程A", 500)).start();
new Thread(new MyThread("線程B", 1000)).start();
new Thread(new MyThread("線程C", 1500)).start();
}
static class MyThread implements Runnable {
/** 線程名稱 */
private String name;
/** 休眠時間 */
private int time;
private MyThread(String name, int time) {
this.name = name;
this.time = time;
}
@Override
public void run() {
try {
// 休眠指定的時間
Thread.sleep(this.time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + "休眠" + this.time + "毫秒。");
}
}
}
線程禮讓
Thread.yield
方法的調用聲明了當前線程已經完成了生命周期中最重要的部分,可以切換給其它線程來執行 。
該方法只是對線程調度器的一個建議,而且也只是建議具有相同優先級的其它線程可以運行。
public class ThreadYieldDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(t, "線程A").start();
new Thread(t, "線程B").start();
}
static class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "運行,i = " + i);
if (i == 2) {
System.out.print("線程禮讓:");
Thread.yield();
}
}
}
}
}
終止線程
Thread
中的stop
方法有缺陷,已廢棄。使用
Thread.stop
停止線程會導致它解鎖所有已鎖定的監視器(由於未經檢查的ThreadDeath
異常會在堆棧中傳播,這是自然的結果)。 如果先前由這些監視器保護的任何對象處於不一致狀態,則損壞的對象將對其他線程可見,從而可能導致任意行為。Thread.stop
的許多用法應由僅修改某些變量以指示目標線程應停止運行的代碼代替。 目標線程應定期檢查此變量,如果該變量指示要停止運行,則應按有序方式從其運行方法返回。如果目標線程等待很長時間(例如,在條件變量上),則應使用中斷方法來中斷等待。
當一個線程運行時,另一個線程可以直接通過 interrupt
方法中斷其運行狀態。
public class ThreadInterruptDemo {
public static void main(String[] args) {
MyThread mt = new MyThread(); // 實例化Runnable子類對象
Thread t = new Thread(mt, "線程"); // 實例化Thread對象
t.start(); // 啟動線程
try {
Thread.sleep(2000); // 線程休眠2秒
} catch (InterruptedException e) {
System.out.println("3、休眠被終止");
}
t.interrupt(); // 中斷線程執行
}
static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("1、進入run()方法");
try {
Thread.sleep(10000); // 線程休眠10秒
System.out.println("2、已經完成了休眠");
} catch (InterruptedException e) {
System.out.println("3、休眠被終止");
return; // 返回調用處
}
System.out.println("4、run()方法正常結束");
}
}
}
如果一個線程的 run
方法執行一個無限循環,並且沒有執行 sleep
等會拋出 InterruptedException
的操作,那么調用線程的 interrupt
方法就無法使線程提前結束。
但是調用 interrupt
方法會設置線程的中斷標記,此時調用 interrupted
方法會返回 true
。因此可以在循環體中使用 interrupted
方法來判斷線程是否處於中斷狀態,從而提前結束線程。
安全地終止線程有兩種方法:
- 定義
volatile
標志位,在run
方法中使用標志位控制線程終止 - 使用
interrupt
方法和Thread.interrupted
方法配合使用來控制線程終止
示例:使用 volatile
標志位控制線程終止
public class ThreadStopDemo2 {
public static void main(String[] args) throws Exception {
MyTask task = new MyTask();
Thread thread = new Thread(task, "MyTask");
thread.start();
TimeUnit.MILLISECONDS.sleep(50);
task.cancel();
}
private static class MyTask implements Runnable {
private volatile boolean flag = true;
private volatile long count = 0L;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 線程啟動");
while (flag) {
System.out.println(count++);
}
System.out.println(Thread.currentThread().getName() + " 線程終止");
}
/**
* 通過 volatile 標志位來控制線程終止
*/
public void cancel() {
flag = false;
}
}
}
示例:使用 interrupt
方法和 Thread.interrupted
方法配合使用來控制線程終止
public class ThreadStopDemo3 {
public static void main(String[] args) throws Exception {
MyTask task = new MyTask();
Thread thread = new Thread(task, "MyTask");
thread.start();
TimeUnit.MILLISECONDS.sleep(50);
thread.interrupt();
}
private static class MyTask implements Runnable {
private volatile long count = 0L;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 線程啟動");
// 通過 Thread.interrupted 和 interrupt 配合來控制線程終止
while (!Thread.interrupted()) {
System.out.println(count++);
}
System.out.println(Thread.currentThread().getName() + " 線程終止");
}
}
}
守護線程
什么是守護線程?
- 守護線程(Daemon Thread)是在后台執行並且不會阻止 JVM 終止的線程。當所有非守護線程結束時,程序也就終止,同時會殺死所有守護線程。
- 與守護線程(Daemon Thread)相反的,叫用戶線程(User Thread),也就是非守護線程。
為什么需要守護線程?
- 守護線程的優先級比較低,用於為系統中的其它對象和線程提供服務。典型的應用就是垃圾回收器。
如何使用守護線程?
- 可以使用
isDaemon
方法判斷線程是否為守護線程。 - 可以使用
setDaemon
方法設置線程為守護線程。- 正在運行的用戶線程無法設置為守護線程,所以
setDaemon
必須在thread.start
方法之前設置,否則會拋出llegalThreadStateException
異常; - 一個守護線程創建的子線程依然是守護線程。
- 不要認為所有的應用都可以分配給守護線程來進行服務,比如讀寫操作或者計算邏輯。
- 正在運行的用戶線程無法設置為守護線程,所以
public class ThreadDaemonDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyThread(), "線程");
t.setDaemon(true); // 此線程在后台運行
System.out.println("線程 t 是否是守護進程:" + t.isDaemon());
t.start(); // 啟動線程
}
static class MyThread implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "在運行。");
}
}
}
}
參考閱讀:Java 中守護線程的總結
FAQ
sleep、yield、join 方法有什么區別
yield
方法yield
方法會 讓線程從Running
狀態轉入Runnable
狀態。- 當調用了
yield
方法后,只有與當前線程相同或更高優先級的Runnable
狀態線程才會獲得執行的機會。
sleep
方法sleep
方法會 讓線程從Running
狀態轉入Waiting
狀態。sleep
方法需要指定等待的時間,超過等待時間后,JVM 會將線程從Waiting
狀態轉入Runnable
狀態。- 當調用了
sleep
方法后,無論什么優先級的線程都可以得到執行機會。 sleep
方法不會釋放“鎖標志”,也就是說如果有synchronized
同步塊,其他線程仍然不能訪問共享數據。
join
join
方法會 讓線程從Running
狀態轉入Waiting
狀態。- 當調用了
join
方法后,當前線程必須等待調用join
方法的線程結束后才能繼續執行。
為什么 sleep 和 yield 方法是靜態的
Thread
類的 sleep
和 yield
方法將處理 Running
狀態的線程。
所以在其他處於非 Running
狀態的線程上執行這兩個方法是沒有意義的。這就是為什么這些方法是靜態的。它們可以在當前正在執行的線程中工作,並避免程序員錯誤的認為可以在其他非運行線程調用這些方法。
Java 線程是否按照線程優先級嚴格執行
即使設置了線程的優先級,也無法保證高優先級的線程一定先執行。
原因在於線程優先級依賴於操作系統的支持,然而,不同的操作系統支持的線程優先級並不相同,不能很好的和 Java 中線程優先級一一對應。
三、線程間通信
當多個線程可以一起工作去解決某個問題時,如果某些部分必須在其它部分之前完成,那么就需要對線程進行協調。
wait/notify/notifyAll
wait
-wait
方法使得線程釋放其占有的對象鎖,讓線程從Running
狀態轉入Waiting
狀態,並等待notify
/notifyAll
來喚醒 。如果沒有釋放鎖,那么其它線程就無法進入對象的同步方法或者同步控制塊中,那么就無法執行notify
或者notifyAll
來喚醒掛起的線程,造成死鎖。notify
- 喚醒一個正在Waiting
狀態的線程,並讓它拿到對象鎖,具體喚醒哪一個線程由 JVM 控制 。notifyAll
- 喚醒所有正在Waiting
狀態的線程,接下來它們需要競爭對象鎖。
注意:
wait
、notify
、notifyAll
都是Object
類中的方法,而非Thread
。wait
、notify
、notifyAll
只能用在synchronized
方法或者synchronized
代碼塊中使用,否則會在運行時拋出IllegalMonitorStateException
。為什么
wait
、notify
、notifyAll
不定義在Thread
中?為什么wait
、notify
、notifyAll
要配合synchronized
使用?首先,需要了解幾個基本知識點:
- 每一個 Java 對象都有一個與之對應的 監視器(monitor)
- 每一個監視器里面都有一個 對象鎖 、一個 等待隊列、一個 同步隊列
了解了以上概念,我們回過頭來理解前面兩個問題。
為什么這幾個方法不定義在
Thread
中?由於每個對象都擁有對象鎖,讓當前線程等待某個對象鎖,自然應該基於這個對象(
Object
)來操作,而非使用當前線程(Thread
)來操作。因為當前線程可能會等待多個線程的鎖,如果基於線程(Thread
)來操作,就非常復雜了。為什么
wait
、notify
、notifyAll
要配合synchronized
使用?如果調用某個對象的
wait
方法,當前線程必須擁有這個對象的對象鎖,因此調用wait
方法必須在synchronized
方法和synchronized
代碼塊中。
生產者、消費者模式是 wait
、notify
、notifyAll
的一個經典使用案例:
public class ThreadWaitNotifyDemo02 {
private static final int QUEUE_SIZE = 10;
private static final PriorityQueue<Integer> queue = new PriorityQueue<>(QUEUE_SIZE);
public static void main(String[] args) {
new Producer("生產者A").start();
new Producer("生產者B").start();
new Consumer("消費者A").start();
new Consumer("消費者B").start();
}
static class Consumer extends Thread {
Consumer(String name) {
super(name);
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == 0) {
try {
System.out.println("隊列空,等待數據");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notifyAll();
}
}
queue.poll(); // 每次移走隊首元素
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 從隊列取走一個元素,隊列當前有:" + queue.size() + "個元素");
}
}
}
}
static class Producer extends Thread {
Producer(String name) {
super(name);
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == QUEUE_SIZE) {
try {
System.out.println("隊列滿,等待有空余空間");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notifyAll();
}
}
queue.offer(1); // 每次插入一個元素
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 向隊列取中插入一個元素,隊列當前有:" + queue.size() + "個元素");
}
}
}
}
}
join
在線程操作中,可以使用 join
方法讓一個線程強制運行,線程強制運行期間,其他線程無法運行,必須等待此線程完成之后才可以繼續執行。
public class ThreadJoinDemo {
public static void main(String[] args) {
MyThread mt = new MyThread(); // 實例化Runnable子類對象
Thread t = new Thread(mt, "mythread"); // 實例化Thread對象
t.start(); // 啟動線程
for (int i = 0; i < 50; i++) {
if (i > 10) {
try {
t.join(); // 線程強制運行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Main 線程運行 --> " + i);
}
}
static class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " 運行,i = " + i); // 取得當前線程的名字
}
}
}
}
管道
管道輸入/輸出流和普通的文件輸入/輸出流或者網絡輸入/輸出流不同之處在於,它主要用於線程之間的數據傳輸,而傳輸的媒介為內存。
管道輸入/輸出流主要包括了如下 4 種具體實現:PipedOutputStream
、PipedInputStream
、PipedReader
和 PipedWriter
,前兩種面向字節,而后兩種面向字符。
public class Piped {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 將輸出流和輸入流進行連接,否則在使用時會拋出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
Print(PipedReader in) {
this.in = in;
}
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
四、線程狀態
java.lang.Thread.State
中定義了 6 種不同的線程狀態,在給定的一個時刻,線程只能處於其中的一個狀態。
以下是各狀態的說明,以及狀態間的聯系:
-
新建(New) - 尚未調用
start
方法的線程處於此狀態。此狀態意味着:創建的線程尚未啟動。 -
可運行(Runnable) - 已經調用了
start
方法的線程處於此狀態。此狀態意味着:線程已經在 JVM 中運行。但是在操作系統層面,它可能處於運行狀態,也可能等待資源調度(例如處理器資源),資源調度完成就進入運行狀態。所以該狀態的可運行是指可以被運行,具體有沒有運行要看底層操作系統的資源調度。 -
阻塞(Blocked) - 請求獲取 monitor lock 從而進入
synchronized
函數或者代碼塊,但是其它線程已經占用了該 monitor lock,所以處於阻塞狀態。要結束該狀態進入Runnable
,從而需要其他線程釋放 monitor lock。此狀態意味着:線程處於被阻塞狀態。 -
等待(Waiting) - 此狀態意味着:線程等待被其他線程顯式地喚醒。 阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取 monitor lock。而等待是主動的,通過調用
Object.wait
等方法進入。進入方法 退出方法 沒有設置 Timeout 參數的 Object.wait
方法Object.notify
/Object.notifyAll
沒有設置 Timeout 參數的 Thread.join
方法被調用的線程執行完畢 LockSupport.park
方法LockSupport.unpark
-
定時等待(Timed waiting) - 此狀態意味着:無需等待其它線程顯式地喚醒,在一定時間之后會被系統自動喚醒。
進入方法 退出方法 Thread.sleep
方法時間結束 設置了 Timeout 參數的 Object.wait
方法時間結束 / Object.notify
/Object.notifyAll
設置了 Timeout 參數的 Thread.join
方法時間結束 / 被調用的線程執行完畢 LockSupport.parkNanos
方法LockSupport.unpark
LockSupport.parkUntil
方法LockSupport.unpark
-
終止(Terminated) - 線程
run
方法執行結束,或者因異常退出了run
方法。此狀態意味着:線程結束了生命周期。
參考資料
- 《Java 並發編程實戰》
- 《Java 並發編程的藝術》
- 進程和線程關系及區別
- Java 線程中 yield 與 join 方法的區別
- sleep(),wait(),yield()和 join()方法的區別
- Java 並發編程:線程間協作的兩種方式:wait、notify、notifyAll 和 Condition
- Java 並發編程:Callable、Future 和 FutureTask
- StackOverflow VisualVM - Thread States
- Java 中守護線程的總結
- Java 並發
- Why must wait() always be in synchronized block