1. 程式人生 > >【Java】多執行緒初探

【Java】多執行緒初探

Java的執行緒狀態

從作業系統的角度看,執行緒有5種狀態:建立, 就緒, 執行, 阻塞, 終止(結束)。如下圖所示

【Java】多執行緒初探

 

而Java定義的執行緒狀態有: 建立(New), 可執行(Runnable), 阻塞(Blocked), 等待(Waiting), 計時等待(Time waiting) 被終止(Terminated)。

那麼相比起作業系統的執行緒狀態, Java定義的執行緒狀態該如何解讀呢? 如下:

1. Java的阻塞、 等待、 計時等待都屬於作業系統中定義的阻塞狀態,不過做了進一步的劃分,阻塞(Blocked)是試圖獲得物件鎖(不是java.util.concurrent庫中的鎖),而物件鎖暫時被其他執行緒持有導致的;等待(Waiting)則是呼叫Object.wait,Thread.join或Lock.lock等方法導致的;計時等待(Time waiting)則是在等待的方法中引入了時間引數進入的狀態,例如sleep(s)

2. Java的Runnable狀態實際上包含了作業系統的就緒和執行這兩種狀態, 但並沒有專門的標識進行區分,而是統一標識為Runnable

獲取當前執行緒的狀態和名稱

currentThread()是Thread類的一個靜態方法,它返回的是當前執行緒物件

對某個執行緒物件有以下方法:

  • getState方法:返回該執行緒的狀態,可能是NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TEMINATED之一
  • getName: 返回執行緒名稱
  • getPriority: 返回執行緒優先順序

下面的例子中,我們通過Thread.currentThread()獲取到當前執行緒物件, 並列印它的狀態,名稱和優先順序:

public class MyThread extends Thread{
 @Override
 public void run() {
 System.out.println("執行緒狀態:" + Thread.currentThread().getState());
 System.out.println("執行緒名稱:" + Thread.currentThread().getName());
 System.out.println("執行緒優先順序:" + Thread.currentThread().getPriority());
 }
 public static void main (String args []) {
 MyThread t = new MyThread();
 t.start();
 }
}

輸出:

執行緒狀態:RUNNABLE
執行緒名稱:Thread-0
執行緒優先順序:5

執行緒的建立和啟動

建立執行緒主要有三種方式:

1 .繼承Thread類

2. 實現runnable介面

3. 使用Callable和Future

對這三種方式,建立執行緒的方式雖然不同,但啟動執行緒的方式是一樣的,都是對執行緒例項呼叫start方法來啟動執行緒。建立執行緒的時候,執行緒處於New狀態,只有呼叫了start方法後,才進入Runnable狀態

一. 繼承Thread類建立執行緒

可以讓當前類繼承父類Thread, 然後例項化當前類就可以建立一個執行緒了

public class MyThread extends Thread {
 private int i = 0;
 public void run () {
 i++;
 System.out.println(i);
 }
 public static void main (String args []){
 MyThread t = new MyThread();
 t.start();
 }
}

輸出

 1

二. 實現Runnable介面建立執行緒

也可以讓當前類繼承Runnable介面, 並將當前類例項化後得到的例項作為引數傳遞給Thread建構函式,從而建立執行緒

MyRunnable.java

public class MyRunnable implements Runnable {
 private int i =0;
 @Override
 public void run() {
 i++;
 System.out.println(i);
 }
}

Test.java

public class Test {
 public static void main (String args[]) {
 Thread t = new Thread(new MyRunnable());
 t.start();
 }
}

輸出

 1

三. 通過Callable介面和Future介面建立執行緒

Callable介面

Callable介面和Runnable介面類似, 封裝一個線上程中執行的任務,區別在於Runnable介面沒有返回值,具體來說就是通過Runnable建立的子執行緒不能給建立它的主執行緒提供返回值。而Callable介面可以讓一個執行非同步任務的子執行緒提供返回值給建立它的主執行緒。 實現Callable需要重寫call方法,call方法的返回值就是你希望回傳給主執行緒的資料。

Future介面

Future可以看作是一個儲存了執行執行緒結果資訊的容器。可以和Callable介面和Runnable介面配合使用。

Future介面中有如下方法:

public interface Future<V> {
 V get () throws ...; // 當任務完成時, 獲取結果
 V get (long timeout, TimeUnit unit); // 在get方法的基礎上指定了超時時間
 void cancel ( boolean mayInterupt); // 取消任務的執行
 boolean isDone (); // 任務是否已完成
 boolean isCancel (); // 任務是否已取消
}

Future對於Callable和Runnable物件的作用

  • 對於Callable物件來說, Future物件可幫助它儲存結果資訊,當呼叫get方法的時候將會發生阻塞, 直到結果被返回。
  • 而對於Runnable物件來說, 無需儲存結果資訊, 所以get方法一般為null, 這裡Future的作用主要是可以呼叫cancel方法取消Runnable任務

FutureTask

FutureTask包裝器是銜接Callable和Future的一座橋樑, 它可以將Callable轉化為一個Runnable和一個Future, 同時實現兩者的介面。 即通過

FutureTask task = new FutureTask(new Callable);

得到的task既是一個Runnable也是一個Future。這樣一來,可以先把得到的task傳入Thread建構函式中建立執行緒並執行(作為Runnable使用), 接著通過task.get以阻塞的方式獲得返回值(作為Future使用)

下面是一個示範例子:

MyCallable.java

import java.util.concurrent.Callable;
 
public class MyCallable implements Callable {
 @Override
 public Object call() throws Exception {
 Thread.sleep(1000);
 return "返回值";
 }
}

Test.java

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Test {
 public static void main (String args []) throws ExecutionException, InterruptedException {
 // task同時實現了Runnable介面和Future介面
 FutureTask task = new FutureTask(new MyCallable());
 // 作為 Runnable 使用
 Thread t = new Thread(task);
 t.start();
 // 作為Future使用, 呼叫get方法時將阻塞直到獲得返回值
 System.out.println(task.get());
 }
}

大約1秒後輸出:

返回值

繼承Thread和實現Runnable介面的區別

總體來說實現Runnable介面比繼承Thread建立執行緒的方式更強大和靈活,體現在以下幾個方面:

1. 可以讓多個執行緒處理同一個資源:實現Runnable的類的例項可以被傳入不同的Thread建構函式建立執行緒, 這些執行緒處理的是該例項裡的同一個資源

2. 更強的擴充套件性:介面允許多繼承,而類只能單繼承, 所以實現Runnable介面擴充套件性更強

3. 適用於執行緒池:執行緒池中只能放入實現了Runnable或者Callable類的執行緒,不能放入繼承Thread的類

Runnable介面和Callable介面的區別

1. 實現Runnable介面要重寫run方法, 實現Callable介面要重寫call方法

2. Callable的call方法有返回值, Runnable的run方法沒有

3. call方法可以丟擲異常, run方法不可以

四.通過執行緒池建立和管理執行緒

在實際的應用中, 通過上面三種方式直接建立執行緒可能會帶來一系列的問題,列舉如下:

<1>. 啟動撤銷執行緒效能開銷大:執行緒的啟動,撤銷會帶來大量開銷,大量建立/撤銷單個的生命週期很短的執行緒將是這一點更加嚴重

<2>. 響應速度慢:從建立執行緒到執行緒被CPU排程去執行任務需要一定時間

<3>. 執行緒難以統一管理

而使用執行緒池能夠解決單獨建立執行緒所帶來的這些問題:

對<1>: 執行緒池通過重用執行緒能減少啟動/撤銷執行緒帶來的效能開銷

對<2>: 相比起臨時建立執行緒,執行緒池提高了任務執行的響應速度

對<3> : 執行緒池能夠統一地管理執行緒。例如1. 控制最大併發數 2.靈活地控制活躍執行緒的數量

3. 實現延遲/週期執行

和執行緒池相關的介面和類

對執行緒池的操作, 要通過執行器(Executor)來實現。通過執行器,可以將Runnable或Callable提交(submit)到執行緒池中執行任務,並在執行緒池用完的時候關閉(shutdown)執行緒池。

(注意:執行緒池和執行器在一定程度上是等效的)

Executor介面

它是執行器的頂層介面, 定義了execute方法

public interface Executor {
 void execute(Runnable command);
}

ExecutorService介面

它是Executor介面的子介面,並對Executor介面進行了擴充套件,下面是部分程式碼

public interface ExecutorService extends Executor {
 void shutdown();
 <T> Future<T> submit(Callable<T> task);
 <T> Future<T> submit(Runnable task, T result);
 // 其他方法
}

對於實現了ExecutorService介面的類的例項:

  • 呼叫submit方法可以將Runnable或Callable例項提交給執行緒池裡的空閒執行緒執行,同時返回一個Future物件, 儲存了和執行結果有關的資訊
  • 當執行緒池用完時, 需要呼叫 shutdown方法關閉執行緒

Executors類

(注意Executor是介面,Executors是類)

Executor是一個儲存著許多靜態的工廠方法的類,這些靜態方法都返回ExecutorService型別的例項

public class Executors {
 public static ExecutorService newFixedThreadPool(int nThreads) {
 return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
 }
 
 public static ExecutorService newCachedThreadPool() {
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
 }
}

Executors的工廠方法彙總

【Java】多執行緒初探

 

執行緒池的一般使用流程

1. 呼叫Executors類中的工廠方法,如newFixedThreadPool獲得執行緒池(執行器)例項

2. 呼叫submit方法提交Runnable物件或Callable物件

3. 如果提交的是Callable物件, 或者提交的是Runnable物件但想要取消,則應該儲存submit方法返回的Future物件

4. 用完執行緒池,呼叫shutdown方法關閉它

執行緒池使用的例子

MyRunnable.java:

public class MyRunnable implements Runnable{
 @Override
 public void run() {
 for (int i=0;i<3;i++) {
 System.out.println("MyRunnable正在執行");
 }
 }
}

MyCallable.java:

import java.util.concurrent.Callable;
 
public class MyCallable implements Callable{
 @Override
 public Object call() throws Exception {
 for (int i=0;i<3;i++) {
 System.out.println("MyCallable正在執行");
 }
 return "回撥引數";
 }
}

Test.java:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Test {
 public static void main (String args []) throws ExecutionException, InterruptedException {
 // 建立一個固定數量為2的執行緒池
 ExecutorService service = Executors.newFixedThreadPool(2);
 // 向執行緒池提交Callable任務,並將結果資訊儲存到Future中
 Future callableFuture = service.submit(new MyCallable());
 // 向執行緒池提交Runnable任務,並將結果資訊儲存到Future中
 Future runnableFuture = service.submit(new MyRunnable());
 // 輸出結果資訊
 System.out.printf("MyCallable, 完成:%b取消:%b返回值:%s%n", callableFuture.isDone(),
 callableFuture.isCancelled(), callableFuture.get());
 System.out.printf("MyRunnable, 完成:%b取消:%b返回值:%s%n", runnableFuture.isDone(),
 runnableFuture.isCancelled(), runnableFuture.get());
 // 關閉執行緒池
 service.shutdown();
 }
}

輸出:

MyCallable正在執行
MyCallable正在執行
MyCallable正在執行
MyCallable, 完成:true取消:false返回值:回撥引數
MyRunnable正在執行
MyRunnable正在執行
MyRunnable正在執行
MyRunnable, 完成:false取消:false返回值:null

執行緒的執行

我們是不能通過呼叫執行緒例項的一個方法去使執行緒處在執行狀態的, 因為排程執行緒執行的工作是由CPU去完成的。通過呼叫執行緒的start方法只是使執行緒處在Runnable即可執行狀態, 處在Runnable狀態的執行緒可能在執行也可能沒有執行(就緒狀態)。

【注意】Java規範中並沒有規定Running狀態, 正在執行的執行緒也是處在Runnable狀態。

執行緒的阻塞(廣義)

開頭介紹執行緒狀態時我們說到, 執行緒的阻塞狀態(廣義)可進一步細分為:阻塞(Blocked), 等待(Waiting), 計時等待(Time waiting) 這三種狀態, 這裡再說明一下:

  • 阻塞(Blocked)是試圖獲得物件鎖(不是java.util.concurrent庫中的鎖),而物件鎖暫時被其他執行緒持有導致
  • 等待(Waiting)則是呼叫Object.wait,Thread.join或Lock.lock等方法導致的
  • 計時等待(Time waiting)則是在等待的方法中引入了時間引數進入的狀態,例如sleep(s)

執行緒的終止

執行緒終止有兩個原因:

1.run方法正常執行結束, 自然終止

2.發生異常但未捕獲, 意外終止

這裡暫不考慮第二種情況, 僅僅考慮第一種情況,則:

正如我們沒有直接的方法呼叫可以讓執行緒處在執行狀態, 我們同樣也沒有直接的方法呼叫可以終止一個執行緒(注:這裡排除了已經廢棄的stop方法),所以,我們要想終止一個執行緒,只能是讓其“自然結束”, 即run方法體內的最後一條語句執行結束, 在這個思路的基礎上,我們有兩種方式可以結束執行緒:

1. 共享變數結束執行緒

2. 使用中斷機制結束執行緒

1. 共享變數結束執行緒

我們可以設定一個共享變數,在run方法體中,判斷該變數為true時則執行有效工作的程式碼,判斷為false時候則退出run方法體。共享變數初始為true, 當想要結束執行緒的時候將共享變數置為false就可以了

優點: 簡單易懂,在非阻塞的情況下能正常工作

缺點: 當執行緒阻塞的時候, 將不會檢測共享變數,執行緒可能不能及時地退出。

執行緒正在執行
發出終止執行緒的訊號
// 約5s後輸出
執行緒終止

如上所示, 我們試圖線上程啟動1秒後就結束執行緒,但實際上在大約5秒後執行緒才結束。這是因為執行緒啟動後因為休眠(sleep)而陷入了阻塞狀態(等待),這時候自然是不會檢測stop變量了。 所以在阻塞狀態下,共享變數結束執行緒的方式可能並不具備良好的時效性

2. 利用中斷機制結束執行緒

因為直接使用共享變數的方式不能很好應對執行緒阻塞的情況,所以我們一般採用中斷機制結束執行緒,單從形式上看,採用中斷機制結束執行緒和共享變數的管理方式

並沒有太大區別,假設t是當前執行緒,則呼叫t.interrupt()會將執行緒中的中斷狀態位置為true, 然後通過t.isInterrupted()可以返回中斷狀態位的值。

區別在於:當剛好遇到執行緒阻塞的時候, 中斷會喚醒阻塞執行緒,這樣的話我們就可以及時的結束執行緒了。

public class InteruptReal implements Runnable{
 @Override
 public void run() {
 try {
 while (!Thread.currentThread().isInterrupted()) {
 System.out.println("執行緒正在執行");
 Thread.sleep(5000);
 }
 } catch (InterruptedException e) {
 // 發生中斷異常後,中斷狀態位會被置為false,這裡不做任何操作
 }
 System.out.println("執行緒已中斷");
 }
 public static void main (String args []) throws InterruptedException {
 Thread t = new Thread(new InteruptReal());
 t.start();
 // 休眠1s
 Thread.sleep(1000);
 System.out.println("發出終止執行緒的訊號");
 t.interrupt();
 }
}

輸出:

執行緒正在執行
發出終止執行緒的訊號
// 立即輸出
執行緒已中斷

執行緒現在已經能夠及時退出啦

中斷執行緒的時候, 如果執行緒處在阻塞狀態,則會1. 喚醒阻塞執行緒,使其重新重新處於RUNNABLE狀態 2. 將中斷狀態位 置為false

注意! 在喚醒阻塞執行緒的同時會將中斷狀態位置為false, 這也許讓人感覺有些奇怪,但這說明了JAVA給了你更多的處理執行緒的自由度。在被阻塞的執行緒喚醒後,你可以選擇再次發起中斷,也可以選擇不中斷。

例子如下: 喚醒阻塞執行緒後, 中斷狀態位會被置為false

public class InteruptReal implements Runnable{
 @Override
 public void run() {
 try {
 while (!Thread.currentThread().isInterrupted()) {
 System.out.println("執行緒正在執行");
 Thread.sleep(5000);
 }
 } catch (InterruptedException e) {
 System.out.println("中斷狀態位:"+Thread.currentThread().isInterrupted());
 }
 
 }
 public static void main (String args []) throws InterruptedException {
 Thread t = new Thread(new InteruptReal());
 t.start();
 // 休眠1s
 Thread.sleep(1000);
 System.out.println("發出中斷");
 t.interrupt();
 }
}

輸出:

執行緒正在執行
發出中斷
中斷狀態位:false

【注意】 Java已經廢棄了執行緒的stop方法, 因為在多執行緒中,它極有可能破壞預期的原子操作, 並因此使得執行緒共享變數取得錯誤的值。

歡迎工作一到八年的Java工程師朋友們加入Java高階交流群:854630135

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有直播平臺和講師直接交流噢

哦對了,喜歡就別忘了關注一下哦~