Java創建線程的方式到底有幾種?

創建線程

創建線程的本質上只有繼承Thread類 實現Runnable接口兩種方式,其他方式如通過線程池創建線程通過Callable 和 FutureTask創建線程通過定時器創建線程等,其本質還是通過上述兩種方式進行創建線程,他們都只不過是包裝了new Thread( )。

多線程的實現方式,在代碼中寫法千變萬化,但是其本質萬變不離其宗。

創建線程的兩種方式(本質)

繼承Thread類

public class ThreadStyle extends Thread{
   //重寫Thread類的run方法
   @Override
   public void run() {
      System.out.println("通過繼承Thread類實現線程");
   }

   public static void main(String[] args) {
      /*
      因爲繼承Thread類之後重寫了Thread類的run方法,所以這裏調用的是繼承Thread類的子類
      重寫的run方法,這裏即ThreadStyle中的run方法
       */
      //直接創建繼承Thread的子類的實例,然後通過實例對象調用start方法開啓線程
      ThreadStyle threadStyle = new ThreadStyle();
      threadStyle.start();
   }
}

實現Runnable接口(推薦)

public class RunnableStyle implements Runnable{
   //實現Runnable接口中的run方法
   @Override
   public void run() {
      System.out.println("通過Runnable接口實現線程");
   }

   public static void main(String[] args) {
      /*
         @Override
         public void run() {
            if (target != null) {
               target.run();
            }
         }
         所以實現Runnable接口的方式實現線程,最終調用的目標對象的run方法,
         這裏即new RunnableStyle()對象的run方法,即上面的run方法
       */
      //傳入Runnable接口的實現類對象作爲target參數值,然後創建一個Thread對象
      Thread thread = new Thread(new RunnableStyle());
      thread.start();
   }
}

其他創建線程的方式(表面)

通過線程池

/**
 * 通過線程池的方式創建線程
 * 本質還是通過繼承Thread類和實現Runnable接口兩種方式
 */
public class ThreadPool5 {

   public static void main(String[] args) {
      /**
       * 深入源碼可以看出,線程池創建線程的本質還是通過new Thread的方法
      public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
       */
      ExecutorService executorService = Executors.newCachedThreadPool();

      for (int i = 0; i < 1000; i++){
         executorService.submit(new Tasktest(){

         });
      }
   }
}

class Tasktest implements Runnable{

   @Override
   public void run() {
      //線程休眠
      try {
         Thread.sleep(500);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      //打印當前線程的名字
      System.out.println(Thread.currentThread().getName());
   }
}

通過Callable 和 FutureTask

圖片

  • Thread類實現了Runnable接口,且Thread類也由Runnable接口組成(聚合關係);
  • RunnableFuture 繼承了 Runnable 和 Future 兩個接口;
  • FutureTask 實現了RunnableFuture 接口,並且由Thread類組成(聚合關係),由Callable 組成(聚合關係);

圖片

  • RunnableFuture 接口繼承了 Runnable接口和Future接口;
  • FutureTask 實現了 RunnableFuture 接口;
  • FutureTask 由Callable 組成

聚合關係:強調整體與部分的關係,整體由部分構成,比如一個部門有多個員工組成;
與組合關係不同的是,整體和部分不是強依賴的,即使整體不存在了,部分依然存在;
例如: 部門撤銷了,員工依然在;
組合關係:與聚合關係一樣,表示整體由部分構成,比如公司有多個部門組成;
但是組合關係是一種強依賴的特殊聚合關係,如果整體不在了,則部門也不在了;
例如:公司不在了,部門也將不在了;
聚合關係:用一條帶空心菱形箭頭的直線表示;
組合關係:用一條帶實心菱形箭頭的直線表示;

參考:
五分鐘讀懂UML類圖
看懂UML類圖和時序圖
類圖

通過上面的類圖分析,可以知道:通過Callable 和 FutureTask創建線程,實質上底層還是通過繼承Thread 和 實現Runnable接口這兩種方式創建線程

主要步驟:

  • (1)創建Callable接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值;
  • (2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象包裝了該Callable對象的call() 方法的返回值;
  • (3)使用FutureTask對象作爲Thread對象的target創建並啓動線程;
  • (4)調用FutureTask對象的get() 方法來獲得子線程執行結束後的返回值;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 通過Callable和FutureTask創建線程
 * 萬變不離其宗
 */
public class CallableandFutureTask implements Callable<Integer> {

   public static void main(String[] args) {
      //創建Callable接口實現類的實例
      CallableandFutureTask callableandFutureTask = new CallableandFutureTask();
      //使用FutureTask包裝Callable對象(其包裝了Callable對象的call()方法的返回值)
      FutureTask<Integer> futureTask = new FutureTask<>(callableandFutureTask);
      for (int i = 0; i < 100; i++){
         System.out.println(Thread.currentThread().getName()+" 的循環變量i的值 "+i);
         if (i == 20){//i等於20的時候開始執行子線程
            //將FutureTask類的實例作爲target傳入Thread類中,創建並啓動線程(類似實現Runnable接口方式創建線程)
            new Thread(futureTask,"有返回值的線程").start();
         }
      }

      try {
         //調用FutureTask對象的get()方法來獲取子線程執行結束後的返回值
         System.out.println("子線程的返回值:"+ futureTask.get());
      } catch (InterruptedException e) {
         e.printStackTrace();
      } catch (ExecutionException e) {
         e.printStackTrace();
      }
   }

   // 實現Callable接口的call方法
   @Override
   public Integer call() throws Exception {
      int i = 0;
      for (; i < 100; i++){
         System.out.println(Thread.currentThread().getName()+" "+i);
      }
      return i;
   }
}

參考:
java創建線程的三種方式及其對比

通過定時器

通過定時器創建線程,本質其實還是通過繼承Thread類 和 Runnable接口兩種方式創建線程

import java.util.Timer;
import java.util.TimerTask;

public class DemoTimerTask {

   public static void main(String[] args) {
      Timer timer = new Timer();
      /*
      用於定時按週期做任務
      第一個參數是task: 表示執行的任務
      第二個參數是delay:表示初始化延時,即初始化延遲多少時間開始執行;
      第三個參數是period:表示每個多少時間執行一次任務(週期)
      注意:這裏的period表示的是相鄰兩個任務開始之間的時間,因此執行時間不會延後
      總結起來就是:啓動後過了delay時間之後,開始以period爲間隔執行task任務

      還需注意schedule和scheduleAtFixedRate的區別
       */
      timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
            System.out.println(Thread.currentThread().getName());
         }
      },5000,1000);
   }
}

說明一下schedule方法和scheduleAtFixedRate方法的區別

  • scheduleAtFixedRate:每次執行時間爲上一次任務開始起向後推一個period間隔,也就是說下次開始執行時間相對於上一次任務開始的時間間隔,因此執行時間不會延後,但是存在任務併發執行的問題。(period:相鄰兩次任務開始之間的時間間隔
  • schedule:每次執行時間爲上一次任務結束後推一個period間隔,也就是說下次開始執行時間相對於上一次任務結束的時間間隔,因此執行時間會不斷延後。(period:上次任務結束之後開始計時,即上次任務結束到下次任務開始之間的間隔
timer.schedule(new TimerTask() {
   @Override
   public void run() {
      System.out.println(Thread.currentThread().getName());
   }
},5000,1000);

參考:
Java定時任務調度詳解

總結

最準確的描述

  • (1)按Oracle文檔來說,創建線程我們通常可以分爲兩類:繼承Thread類 和 實現Runnable接口。
  • (2)準確地講,創建線程只有一種方式那就是構造Thread類,而實現線程的執行單元(實現類裏面的run方法)有兩種方式
    • 實現Runnable接口的 run 方法,並把Runnable實例傳給 Thread 類;
    • 重寫Thread的 run 方法(繼承 Thread 類);

兩者的本質區別

  • 繼承Thread類方式:繼承Thread類的子類必須重寫Thread類中的run方法,所以最終調用的也是重寫之後的run方法;
  • 實現Runnable接口方式:實現Runnable接口中的run方法,然後將Runnable接口的實現類對象傳入Thread類,所以最終調用的是Runnable接口的實現類中的run方法;

優缺點

實現Runnable接口方式相比繼承Thread類的優點:

  • (1)更有利於代碼解耦合;
  • (2)能夠繼承其他類,可拓展性更好;
  • (3)更容易共享資源(變量);

繼承Thread類的子類的內部變量不會直接共享

  • (4)損耗小;

當要新建一個任務時,如果是繼承Thread類的方式,則需要new一個類的對象,但是這樣做的話損耗比較大,這樣我們每次都需要去新建一個線程,執行完之後還需要去銷燬。但是如果我們採用實現Runnable接口的方式,傳入target,傳入實現Runnable接口的類的實例的方法,這樣我們就可以反覆地利用同一個線程。比如,線程池就是這樣做的。
總結:
繼承Thread類的方式,線程不可複用;
實現Runnable接口的方式,線程可以複用;

繼承Thread類方式也有幾個小小的好處,但相對於其缺點來說,其優點不值一提:

  • (1)在 run() 方法內部獲取當前線程可以直接用 this,而無須用 Thread.currentThread( ) 方法;
  • (2)繼承Thread類的子類的內部變量不會直接共享,少數不需要共享變量的場景下使用起來會更加方便;

兩種方法同時使用會怎樣

public class BothRunnableThread {

   public static void main(String[] args) {
      //使用匿名內部類(兩個:一個是Runnable類,一個是Thread類)
      new Thread(new Runnable() {
         //實現Runnable接口的run方法
         @Override
         public void run() {
            System.out.println("來自Runnable接口的實現類的run方法!");
         }
      }){
         //重寫父類Thread類的run方法
         @Override
         public void run() {
            System.out.println("來自繼承Thread的子類重寫之後的run方法");
         }
      }.start();
   }
}

輸出結果:

來自繼承Thread的子類重寫之後的run方法

因爲繼承Thread類的子類重寫了run方法,調用的時候重寫的run方法會覆蓋Runnable接口實現類中實現的run方法,所以最終調用的是重寫的run方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章