本文首發自「慕課網」,想了解更多IT幹貨内容,程式員圈内熱聞,歡迎關注!
作者| 慕課網精英講師 ColorfulC
Java 多線程
本篇文章我們介紹一下如何建立線程,建立線程有哪幾種方式,線程的狀态、生命周期等内容。
1. 什麼是線程
要了解什麼是線程,就要先了解程序的概念。
程序,是指計算機中已運作的程式,它是一個動态執行的過程。假設我們電腦上同時運作了浏覽器、QQ 以及代碼編輯器三個軟體,這三個軟體之是以同時運作,就是程序所起的作用。
線程是作業系統能夠進行運算排程的最小機關。大部分情況下,它被包含在程序之中,是程序中的實際運作機關。也就是說一個程序可以包含多個線程, 是以線程也被稱為輕量級程序。
如果你還是對于程序和線程的概念有所困惑,推薦一篇比較優秀的文章,有助于幫助你了解程序和線程的概念。
2. 建立線程
在 Java 中,建立線程有以下 3 種方式:
- 繼承 Thread 類,重寫 run() 方法,該方法代表線程要執行的任務;
- 實作 Runnable 接口,實作 run() 方法,該方法代表線程要執行的任務;
- 實作 Callable 接口,實作 call() 方法,call() 方法作為線程的執行體,具有傳回值,并且可以對異常進行聲明和抛出。
下面我們分别來看下這 3 種方法的具體實作。
2.1 Thread 類
Thread 類是一個線程類,位于 java.lang 包下。
2.1.1 構造方法
Thread 類的常用構造方法如下:
- Thread():建立一個線程對象;
- Thread(String name):建立一個指定名稱的線程對象;
- Thread(Runnable target):建立一個基于 Runnable 接口實作類的線程對象;
- Thread(Runnable target, String name):建立一個基于 Runnable 接口實作類,并具有指定名稱的線程對象。
2.1.2 常用方法
void run():線程相關的代碼寫在該方法中,一般需要重寫;
void start():啟動目前線程;
static void sleep(long m):使目前線程休眠 m 毫秒;
void join():優先執行調用 join() 方法的線程。
Tips:run() 方法是一個非常重要的方法,它是用于編寫線程執行體的方法,不同線程之間的一個最主要差別就是 run() 方法中的代碼是不同的。
可翻閱官方文檔以檢視更多 API。
2.1.3 執行個體
通過繼承 Thread 類建立線程可分為以下 3 步:
- 定義 Thread 類的子類,并重寫該類的 run() 方法。run() 方法的方法體就代表了線程要完成的任務;
- 建立 Thread 子類的執行個體,即建立線程對象;
- 調用線程對象的 start 方法來啟動該線程。
具體執行個體如下:
/**
* @author colorful@TaleLin
*/
public class ThreadDemo1 extends Thread {
/**
* 重寫 Thread() 的方法
*/
@Override
public void run() {
System.out.println("這裡是線程體");
// 目前列印線程的名稱
System.out.println(getName());
}
public static void main(String[] args) {
// 執行個體化 ThreadDemo1 對象
ThreadDemo1 threadDemo1 = new ThreadDemo1();
// 調用 start() 方法,以啟動線程
threadDemo1.start();
}
}
代碼塊1234567891011121314151617181920212223
運作結果:
這裡是線程體
Thread-0
代碼塊12
小夥伴們可能會有疑問,上面這樣的代碼,和普通的類執行個體化以及方法調用有什麼差別的,下面我們來看一個稍微複雜些的執行個體:
/**
* @author colorful@TaleLin
*/
public class ThreadDemo2 {
/**
* 靜态内部類
*/
static class MyThread extends Thread {
private int i = 3;
MyThread(String name) {
super(name);
}
@Override
public void run() {
while (i > 0) {
System.out.println(getName() + " i = " + i);
i--;
}
}
}
public static void main(String[] args) {
// 建立兩個線程對象
MyThread thread1 = new MyThread("線程1");
MyThread thread2 = new MyThread("線程2");
// 啟動線程
thread1.start();
thread2.start();
}
}
代碼塊123456789101112131415161718192021222324252627282930313233343536
運作結果:
線程2 i = 3
線程1 i = 3
線程1 i = 2
線程2 i = 2
線程1 i = 1
線程2 i = 1
代碼塊123456
代碼中我們是先啟動了線程 1,再啟動了線程 2 的,觀察運作結果,線程并不是按照我們所預想的順序執行的。這裡就要劃重點了,不同線程,執行順序是随機的。如果你再執行幾次代碼,可以觀察到每次的運作結果都可能不同:
2.2 Runnable 接口
2.2.1 為什麼需要Runnable接口
通過實作 Runnable 接口的方案來建立線程,要優于繼承 Thread 類的方案,主要有以下原因:
- Java 不支援多繼承,所有的類都隻允許繼承一個父類,但可以實作多個接口。如果繼承了 Thread 類就無法繼承其它類,這不利于擴充;
- 繼承 Thread 類通常隻重寫 run() 方法,其他方法一般不會重寫。繼承整個 Thread 類成本過高,開銷過大。
2.2.2 執行個體
通過實作 Runnable 接口建立線程的步驟如下:
- 定義 Runnable 接口的實作類,并實作該接口的 run() 方法。這個 run() 方法的方法體同樣是該線程的線程執行體;
- 建立 Runnable 實作類的執行個體,并以此執行個體作為 Thread 的 target 來建立 Thread 對象,該 Thread 對象才是真正的線程對象;
- 調用線程對象的 start 方法來啟動該線程。
具體執行個體如下:
/**
* @author colorful@TaleLin
*/
public class RunnableDemo1 implements Runnable {
private int i = 5;
@Override
public void run() {
while (i > 0) {
System.out.println(Thread.currentThread().getName() + " i = " + i);
i--;
}
}
public static void main(String[] args) {
// 建立兩個實作 Runnable 實作類的執行個體
RunnableDemo1 runnableDemo1 = new RunnableDemo1();
RunnableDemo1 runnableDemo2 = new RunnableDemo1();
// 建立兩個線程對象
Thread thread1 = new Thread(runnableDemo1, "線程1");
Thread thread2 = new Thread(runnableDemo2, "線程2");
// 啟動線程
thread1.start();
thread2.start();
}
}
代碼塊12345678910111213141516171819202122232425262728
運作結果:
線程1 i = 5
線程1 i = 4
線程1 i = 3
線程1 i = 2
線程2 i = 5
線程1 i = 1
線程2 i = 4
線程2 i = 3
線程2 i = 2
線程2 i = 1
代碼塊12345678910
2.3 Callable 接口
2.3.1 為什麼需要Callable接口
繼承 Thread 類和實作 Runnable 接口這兩種建立線程的方式都沒有傳回值。是以,線程執行完畢後,無法得到執行結果。為了解決這個問題,Java 5 後,提供了 Callable 接口和 Future 接口,通過它們,可以線上程執行結束後,傳回執行結果。
2.3.2 執行個體
通過實作 Callable 接口建立線程步驟如下:
- 建立 Callable 接口的實作類,并實作 call() 方法。這個 call() 方法将作為線程執行體,并且有傳回值;
- 建立 Callable 實作類的執行個體,使用 FutureTask 類來包裝 Callable 對象,這個 FutureTask 對象封裝了該 Callable 對象的 call() 方法的傳回值;
- 使用 FutureTask 對象作為 Thread 對象的 target 建立并啟動新線程;
- 調用 FutureTask 對象的 get() 方法來獲得線程執行結束後的傳回值。
具體執行個體如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author colorful@TaleLin
*/
public class CallableDemo1 {
static class MyThread implements Callable<String> {
@Override
public String call() { // 方法傳回值類型是一個泛型,在上面 Callable<String> 處定義
return "我是線程中傳回的字元串";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 常見實作類的執行個體
Callable<String> callable = new MyThread();
// 使用 FutureTask 類來包裝 Callable 對象
FutureTask<String> futureTask = new FutureTask<>(callable);
// 建立 Thread 對象
Thread thread = new Thread(futureTask);
// 啟動線程
thread.start();
// 調用 FutureTask 對象的 get() 方法來獲得線程執行結束後的傳回值
String s = futureTask.get();
System.out.println(s);
}
}
代碼塊123456789101112131415161718192021222324252627282930313233
運作結果:
我是線程中傳回的字元串
代碼塊1
3. 線程休眠
在前面介紹 Thread 類的常用方法時,我們介紹了 sleep() 靜态方法,該方法可以使目前執行的線程睡眠(暫時停止執行)指定的毫秒數。
線程休眠的執行個體如下:
/**
* @author colorful@TaleLin
*/
public class SleepDemo implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i ++) {
// 列印語句
System.out.println(Thread.currentThread().getName() + ":執行第" + i + "次");
try {
// 使目前線程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 執行個體化 Runnable 的實作類
SleepDemo sleepDemo = new SleepDemo();
// 執行個體化線程對象
Thread thread = new Thread(sleepDemo);
// 啟動線程
thread.start();
}
}
代碼塊123456789101112131415161718192021222324252627282930
運作結果:
Thread-0:執行第1次
Thread-0:執行第2次
Thread-0:執行第3次
Thread-0:執行第4次
Thread-0:執行第5次
代碼塊12345
4. 線程的狀态和生命周期
java.lang.Thread.Starte 枚舉類中定義了 6 種不同的線程狀态:
- NEW:建立狀态,尚未啟動的線程處于此狀态;
- RUNNABLE:可運作狀态,Java 虛拟機中執行的線程處于此狀态;
- BLOCK:阻塞狀态,等待螢幕鎖定而被阻塞的線程處于此狀态;
- WAITING:等待狀态,無限期等待另一線程執行特定操作的線程處于此狀态;
- TIME_WAITING:定時等待狀态,在指定等待時間内等待另一線程執行操作的線程處于此狀态;
- TERMINATED:結束狀态,已退出的線程處于此狀态。
值得注意的是,一個線程在給定的時間點隻能處于一種狀态。這些狀态是不反映任何作業系統線程狀态的虛拟機狀态。
線程的生命周期,實際上就是上述 6 個線程狀态的轉換過程。下圖展示了一個完整的生命周期:
5. 小結
通過本篇文章,我們知道了線程是作業系統能夠進行運算排程的最小機關。線程也被稱為輕量級程序。在 Java 中,可以以 3 種方式建立線程,分别是繼承 Thread 類、實作 Runnable 接口以及實作 Callable 接口。可以使用靜态方法 sleep() 讓線程休眠。線程狀态有 6 種,也有資料上說線程有 5 種,這部分内容我們按照 Java 源碼中的定義 6 種來記憶即可。
歡迎關注「慕課網」,發現更多IT圈優質内容,分享幹貨知識,幫助你成為更好的程式員!