一、線程簡介
什麼是程序?
簡言之,程序可視為一個正在運作的程式。它是系統運作程式的基本機關,是以程序是動态的。程序是具有一定獨立功能的程式關于某個資料集合上的一次運作活動。程序是作業系統進行資源配置設定的基本機關。
什麼是線程?
線程是作業系統進行排程的基本機關。線程也叫輕量級程序(Light Weight Process),在一個程序裡可以建立多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,并且能夠通路共享的記憶體變量。
程序和線程的差別
一個程式至少有一個程序,一個程序至少有一個線程。
線程比程序劃分更細,是以執行開銷更小,并發性更高。
程序是一個實體,擁有獨立的資源;而同一個程序中的多個線程共享程序的資源。
二、線程基本用法
線程(Thread)基本方法清單:
建立線程
建立線程有三種方式:
繼承 Thread 類
實作 Runnable 接口
實作 Callable 接口
通過繼承 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 接口優于繼承 Thread 類,因為:
Java 不支援多重繼承,所有的類都隻允許繼承一個父類,但可以實作多個接口。如果繼承了 Thread 類就無法繼承其它類,這不利于擴充。
類可能隻要求可執行就行,繼承整個 Thread 類開銷過大。
通過實作 Runnable 接口建立線程的步驟:
定義 Runnable 接口的實作類,并覆寫該接口的 run 方法。該 run 方法的方法體同樣是該線程的線程執行體。
建立 Runnable 實作類的執行個體,并以此執行個體作為 Thread 的 target 來建立 Thread 對象,該 Thread 對象才是真正的線程對象。
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--;
}
}
}
}
繼承 Thread 類 和 實作 Callable 接口這兩種建立線程的方式都沒有傳回值。是以,線程執行完後,無法得到執行結果。但如果期望得到執行結果該怎麼做?
為了解決這個問題,Java 1.5 後,提供了 Callable 接口和 Future 接口,通過它們,可以線上程執行結束後,傳回執行結果。
通過實作 Callable 接口建立線程的步驟:
1、建立 Callable 接口的實作類,并實作 call 方法。該 call 方法将作為線程執行體,并且有傳回值。
2、建立 Callable 實作類的執行個體,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call 方法的傳回值。
3、使用 FutureTask 對象作為 Thread 對象的 target 建立并啟動新線程。
4、調用 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() + "在運作。");
}
}
}
}
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 方法讓一個線程強制運作,線程強制運作期間,其他線程無法運作,必須等待此線程完成之後才可以繼續執行。
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 等方法進入。
定時等待(Timed waiting) - 此狀态意味着:無需等待其它線程顯式地喚醒,在一定時間之後會被系統自動喚醒。

終止(Terminated) - 線程 run 方法執行結束,或者因異常退出了 run 方法。此狀态意味着:線程結束了生命周期。
作者:靜默虛空
連結:
https://juejin.im/post/5e02b3d6518825127324b032來源:掘金