天天看點

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

文章目錄

  • 建立線程
    • 建立線程的兩種方式(本質)
      • 繼承Thread類
      • 實作Runnable接口(推薦)
    • 其他建立線程的方式(表面)
      • 通過線程池
      • 通過Callable 和 FutureTask
      • 通過定時器
    • 總結
      • 最準确的描述
      • 兩者的本質差別
      • 優缺點
    • 兩種方法同時使用會怎樣

建立線程

建立線程的本質上隻有繼承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

Java建立線程的方式到底有幾種?建立線程
  • Thread類實作了Runnable接口,且Thread類也由Runnable接口組成(聚合關系);
  • RunnableFuture 繼承了 Runnable 和 Future 兩個接口;
  • FutureTask 實作了RunnableFuture 接口,并且由Thread類組成(聚合關系),由Callable 組成(聚合關系);
Java建立線程的方式到底有幾種?建立線程
  • 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方法。