Java高併發與多執行緒(二)-----執行緒的實現方式

流年的夏天發表於2021-01-18

今天,我們開始Java高併發與多執行緒的第二篇,執行緒的實現方式

   

通常來講,執行緒有三種基礎實現方式,一種是繼承Thread類,一種是實現Runnable介面,還有一種是實現Callable介面,當然,如果我們鋪開,擴充套件一下,會有很多種實現方式,但是歸根溯源,其實都是這幾種實現方式的衍生和變種。

我們依次來講。

   

【第一種 · 繼承Thread】

繼承Thread之後,要實現父類的run方法,然後在起執行緒的時候,呼叫其start方法。

 1 public class DemoThreadDemoThread extends Thread {
 2     public void run() {
 3         System.out.println("我執行了一次!");
 4     }
 5 
 6     public static void main(String[] args) throws InterruptedException {
 7         for (int i = 0; i < 3; i++) {
 8             Thread thread = new DemoThreadDemoThread();
 9             thread.start();
10             Thread.sleep(3000);
11         }
12     }
13 }

這裡,有些同學對start方法和run方法的作用理解有點問題,解釋一下:

start()方法被用來啟動新建立的執行緒,在start()內部呼叫了run()方法

當你呼叫run()方法的時候,其實就和呼叫一個普通的方法沒有區別。

   

【第二種 · 實現Runnable介面】

第二種方法,是實現Runnable介面的run方法,在起執行緒的時候,如下,new一個Thread類,然後把類當做引數傳進去。

 1 public class DemoThreadDemoRunnable implements Runnable {
 2     public void run() {
 3         System.out.println("我執行了一次!");
 4     }
 5 
 6     public static void main(String[] args) throws InterruptedException {
 7         for (int i = 0; i < 3; i++) {
 8             Thread thread = new Thread(new DemoThreadDemoRunnable());
 9             thread.start();
10             Thread.sleep(3000);
11         }
12     }
13 } 

 

   

  • 兩種基礎實現方式的選擇

  這兩種方法看起來其實差不多,但是平時使用的時候我們如何做選擇呢?

一般而言,我們選擇Runnable會好一點;

首先,我們從程式碼的架構考慮。

實際上,Runnable 裡只有一個 run() 方法,它定義了需要執行的內容,在這種情況下,實現了 Runnable Thread 類的解耦Thread 類負責執行緒啟動和屬性設定等內容,權責分明。

   

第二點就是在某些情況下可以提高效能

使用繼承 Thread 類方式,每次執行一次任務,都需要新建一個獨立的執行緒,執行完任務後執行緒走到生命週期的盡頭被銷燬,如果還想執行這個任務,就必須再新建一個繼承了 Thread 類的類,如果此時執行的內容比較少,比如只是在 run() 方法裡簡單列印一行文字,那麼它所帶來的開銷並不大,相比於整個執行緒從開始建立到執行完畢被銷燬,這一系列的操作比 run() 方法列印文字本身帶來的開銷要大得多。

如果我們使用實現 Runnable 介面的方式,就可以把任務直接傳入執行緒池使用一些固定的執行緒來完成任務,不需要每次新建銷燬執行緒,大大降低了效能開銷。

   

第三點好處在於可以繼承其他父類

Java 語言不支援雙繼承,如果我們的類一旦繼承了 Thread 類,那麼它後續就沒有辦法再繼承其他的類,這樣一來,如果未來這個類需要繼承其他類實現一些功能上的擴充,它就沒有辦法做到了,相當於限制了程式碼未來的可擴充性。

   

  • 為什麼說Runnable和Thread其實是同一種建立執行緒的方法

  首先,我們可以去看看Thread的原始碼,我們可以看到:

其實Thread類也是實現了Runnable介面,以下是Thread的run方法:

其中的target指的就是你是實現了Runnable介面的類:

Thread被new出來之後,呼叫Thread的start方法,會呼叫其run方法,然後其實執行的就是Runnable介面的run方法(實現後的方法)。

所以,當你繼承了Thread之後,你實現的run方法其實還是Runnable介面的run方法,

因此,其實無返回值執行緒的實現方式只有一種,那就是實現Runnable介面的run方法,然後呼叫Thread的start方法,start方法內部會呼叫你寫的run方法,完成程式碼邏輯。

   

【第三種 · 實現Callable介面】

第 3 種執行緒建立方式是通過有返回值的 Callable 建立執行緒

Callable介面實際上是屬於Executor框架中的功能類,之後的部分會詳細講述Executor框架。

   

Callable和Runnable可以認為是兄弟關係,Callable的call()方法類似於Runnable介面中run()方法,都定義任務要完成的工作,實現這兩個介面時要分別重寫這兩個方法;

主要的不同之處是call()方法是有返回值的,執行Callable任務可以拿到一個Future物件,表示非同步計算的結果。

我們通過Future物件可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果

   

程式碼示例如下:

 1 public class DemoThreadDemoCallable implements Callable<Integer> {
 2 
 3     private volatile static int count = 0;
 4 
 5     public Integer call() {
 6         System.out.println("我是callable,我執行了一次");
 7         return count++;
 8     }
 9 
10     public static void main(String[] args) throws InterruptedException, ExecutionException {
11         List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
12 
13         for (int i = 0; i < 3; i++) {
14             FutureTask<Integer> futureTask = new FutureTask<Integer>(new DemoThreadDemoCallable());
15             taskList.add(futureTask);
16         }
17 
18         for (FutureTask<Integer> task : taskList) {
19             Thread.sleep(1000);
20             new Thread(task).start();
21         }
22 
23         // 一般這個時候是做一些別的事情
24         Thread.sleep(3000);
25         for (FutureTask<Integer> task : taskList) {
26             if (task.isDone()) {
27                 System.out.printf("我是第%s次執行的!\n", task.get());
28             }
29         }
30     }
31 }

 

執行結果如下:

其中,count 使用volatile 修飾了一下,關於volatile 關鍵字,之後會再次講到,這裡先不說。

   

當然了,我們一般情況下,都會使用執行緒池來進行Callable的呼叫,如下程式碼所示。

 1 public class DemoThreadDemoCallablePool {
 2     public static void main(String[] args) {
 3         ExecutorService threadPool = Executors.newSingleThreadExecutor();
 4         Future<Integer> future = threadPool.submit(new DemoThreadDemoCallable());
 5         try {
 6             System.out.println(future.get());
 7         } catch (Exception e) {
 8             System.err.print(e.getMessage());
 9         } finally {
10             threadPool.shutdown();
11         }
12     }
13 }

 

   

  • Callable和Runnable的區別

  最後,我們來看看Callable和Runnable的區別:

    1. Callable規定的方法是call(),而Runnable規定的方法是run()
    2. Callable的任務執行後可返回值,而Runnable的任務是不能返回值的(執行Callable任務可拿到一個Future物件, Future表示非同步計算的結果)
    3. call()方法可丟擲異常,而run()方法是不能丟擲異常的

   

【三種建立執行緒的基礎方式圖示】

通過圖示基本上可以看得很清楚,Callable是需要Future介面的實現類搭配使用的;

Future介面一共有5個方法:

    • boolean cancel(boolean mayInterruptIfRunning)

  用於取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。

  引數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設定true,則表示可以取消正在執行過程中的任務。

  如果任務已經完成,此方法返回false,即取消已經完成的任務會返回false;

  如果任務正在執行,若mayInterruptIfRunning設定為true,則返回true,若mayInterruptIfRunning設定為false,則返回false;

  如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。

   

    • boolean isCancelled()

  如果在Callable任務正常完成前被取消,返回True

   

    • boolean isDone()

  若Callable任務完成,返回True

   

    • V get() throws InterruptedException, ExecutionException

  返回Callable裡call()方法的返回值,呼叫這個方法會導致程式阻塞,必須等到子執行緒結束後才會得到返回值

   

    • V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

  用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null

   

事實上,FutureTask是Future介面唯一一個實現類。(只看了1.7和1.8,後面的版本沒有check

   

【JVM執行緒模型】

在上一節的時候我們講了執行緒的生命週期和狀態,其實每一個執行緒,從建立到銷燬,都佔用了大量的CPU和記憶體,但是真正執行任務鎖佔用的資源可能並沒有很多,可能只能佔用到裡面很少的一部分。

在java裡面,使用執行緒執行非同步任務的時候,如果每次都建立一個新的執行緒,那麼系統資源會大量被佔用,伺服器會一直處於超高負荷的運算,最終很容易就會造成系統崩潰。

   

從jdk1.5開始,java將工作單元和執行機制分離,工作單元主要包括Runnable和Callable,執行機制就是Executor框架。

在HotSpot VM的執行緒模型中,Java執行緒會被一對一的對映成本地作業系統執行緒。(Java執行緒建立的時候,作業系統裡面會對應建立一個執行緒,Java執行緒被銷燬,作業系統中的執行緒也被銷燬)

   

在應用層,Java多執行緒程式會將整個應用分解成多個任務,然後執行的時候,會使用Executor將這些任務分配給固定的一些執行緒去分別執行,而底層作業系統核心會將這些執行緒對映到硬體處理器上,呼叫CPU進行執行。

類似下圖:

   

Executor框架

以上,是Executor框架結構圖解。

Executor框架包括3大部分:

    • 任務

  也就是工作單元,包括被執行任務需要實現的介面(Runnable/Callable)

    • 任務的執行

  把任務分派給多個執行緒的執行機制,包括Executor介面及繼承自Executor介面的ExecutorService介面。

    • 非同步計算的結果

  包括Future介面及實現了Future介面的FutureTask類。

   

    • Executor

  Executor框架的基礎,將任務的提交和執行分離開。

   

    • TreadPoolExecutor

  執行緒池的核心實現類,用來執行被提交的任務。

  通常使用Executors建立,一共有三種型別的ThreadPoolExecutor:

      • SingleThreadExecutor(單執行緒)
      • FixedThreadPool(限制當前執行緒數量)
      • CachedThreadPool(大小無界的執行緒池)

   

    • ScheduledThreadPoolExecutor

  實現類,可以在一個給定的延遲時間後,執行命令,或者定期執行。

  通常也是由Executor建立,一共有兩種型別的ScheduledThreadPoolExecutor。

      • ScheduledThreadPoolExecutor(包含多個執行緒,週期性執行)
      • SingleThreadScheduledExecutor(只包含一個執行緒)

   

    • Future

  非同步計算的結果。

  這裡在很多時候返回的是一個FutureTask的物件,但是並不是必須是FutureTask,只要是Future的實現類即可

   

【TreadPoolExecutor】

之前已經說過,TreadPoolExecutor是執行緒池的核心實現類,所有執行緒池的底層實現都依靠它的建構函式

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {

 

我們可以依次解釋一下:

  • corePoolSize

  核心執行緒數,也叫常駐執行緒數,執行緒池裡從建立之後會一直存在的執行緒數量。

  • maximumPoolSize

  最大執行緒數,執行緒池中最多存在的執行緒數量。

  • keepAliveTime

  空閒執行緒的存活時間,一個執行緒執行完任務之後,等待多久會被執行緒池回收。

  • unit

  keepAliveTime引數的單位

  • workQueue

  執行緒佇列(一般為阻塞佇列)

  • threadFactory

  執行緒工廠,用於建立執行緒

  • handler

  拒絕策略,由於達到執行緒邊界和佇列容量而阻止執行時使用的處理程式。

   

其實,執行緒池不能算作一種建立執行緒的方式,為什麼呢?

對於執行緒池而言,本質上是通過執行緒工廠建立執行緒的。

預設採用 DefaultThreadFactory ,它會給執行緒池建立的執行緒設定一些預設值,比如:執行緒的名字、是否是守護執行緒,以及執行緒的優先順序等。


但是無論怎麼設定這些屬性,最終它還是通過 new Thread() 建立執行緒,只不過這裡的建構函式傳入的引數要多一些,由此可以看出通過執行緒池建立執行緒並沒有脫離最開始的那兩種基本的建立方式,因為本質上還是通過 new Thread() 實現的。

   

關於執行緒池的詳細概念內容,可先參考我的另外一篇部落格:執行緒池略略觀,後面也會再說。

   

關於執行緒的主要建立方式,大概就是以上這些,下一篇,會講執行緒的基本屬性和主要方法。

相關文章