詳細分析 Java 中實現多線程的方法有幾種?(從本質上出發)

目錄php

  • 詳細分析 Java 中實現多線程的方法有幾種?(從本質上出發)java

    • 正確的說法(從本質上出發)面試

    • 經典錯誤說法(從本質上出發)spring

詳細分析 Java 中實現多線程的方法有幾種?(從本質上出發)

正確的說法(從本質上出發)

  • 實現多線程的官方正確方法: 2 種。

  • Oracle 官網的文檔說明c#

  • 方法小結多線程

    • 方法一: 實現 Runnable 接口。架構

    • 方法二: 繼承 Thread 類。ide

  • 代碼示例工具

    /**
     * <p>
     * 實現 Runnable 接口的方式建立線程
     * </p>
     *
     * @author 踏雪彡尋梅
     * @version 1.0
     * @date 2020/9/7 - 00:34
     * @since JDK1.8
     */
    public class RunnableStyle implements Runnable {
        @Override
        public void run() {
            System.out.println("用 Runnable 方式實現線程~~~");
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new RunnableStyle());
            thread.start();
        }
    }
    /**
     * <p>
     * 繼承 Thread 類的方式建立線程
     * </p>
     *
     * @author 踏雪彡尋梅
     * @version 1.0
     * @date 2020/9/7 - 00:37
     * @since JDK1.8
     */
    public class ThreadStyle extends Thread {
        @Override
        public void run() {
            System.out.println("用 Thread 方式實現線程~~~");
        }
    
        public static void main(String[] args) {
            new ThreadStyle().start();
        }
    }
  • 兩種方式的對比學習

    • 方法一(實現 Runnable 接口)更好。

    • 方法二的缺點:

    • 從代碼的架構去考慮,具體執行的任務也就是 run 方法中的內容,它應該和線程的建立、運行的機制也就是 Thread 類是解耦的。因此不該該把他們混爲一談。從解耦的角度,方法一更好。

    • 該方法每次若是想新建一個任務,只能去新建一個獨立的線程,而新建一個獨立的線程這樣的損耗是比較大的,它須要去建立、而後執行,執行完了還要銷燬;而若是使用 Runnable 接口的方式,咱們就能夠利用線程池之類的工具,利用這些工具就能夠大大減少這些建立線程、銷燬線程所帶來的損耗。因此方法一相比於方法二的這一點,好在資源的節約上。

    • 繼承了 Thread 類以後,因爲 Java 不支持雙繼承,那麼這個類就沒法繼承其餘的類了,這大大限制了咱們的可擴展性。

  • 兩種方式的本質區別

  • 方法一: 最終調用 target.run; ,經過如下兩圖能夠知道使用這個方法時其實是傳遞了一個 target 對象,執行了這個對象的 run 方法。

    方法二: run() 整個都被重寫。一旦子類重寫了父類的方法,原有方法會被覆蓋被拋棄,即如下代碼不會被此次調用所採納。

    綜上,兩種方法都是執行了 run 方法,只不過 run 方法的來源不一樣。

  • 同時使用兩種方法會怎樣?

  • 代碼演示

/**
* <p>
* 同時使用 Runnable 和 Thread 兩種實現線程的方式
* </p>
*
* @author 踏雪彡尋梅
* @version 1.0
* @date 2020/9/7 - 22:38
* @since JDK1.8
*/
@SuppressWarnings("all")
public class BothRunnableThread {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我來自 Runnable。。。");
            }
        }) {
            @Override
            public void run() {
                System.out.println("我來自 Thread。。。");
            }
        }.start();
    }
}

運行結果

分析

首先建立了一個匿名內部類 Thread。傳入了一個 Runnable 對象。

而後重寫了 Thread 的 run 方法。最後啓動線程。

由於重寫了 Thread 的 run 方法,因此它父類的 run 方法就被覆蓋掉了,因此即使傳入了 Runnable 對象也不會執行它。

  • 總結

  • 方法一: 實現 Runnable 接口的 run 方法,並把 Runnable 實例傳給 Thread 類,再讓 Thread 類去執行這個 run 方法。

    方法二: 重寫 Thread 的 run 方法(繼承 Thread 類)。

    從以上的分析中,準確的講建立線程只有一種方式那就是構造 Thread 類,而實現線程的執行單元有兩種方式(run 方法的兩種不一樣實現狀況)。

經典錯誤說法(從本質上出發)

1.」線程池建立線程也算是一種新建線程的方式。「

  1. 經過線程池中的源碼,能夠知道線程池建立線程的方式本質上也是經過構造 Thread 的方式建立的。因此線程池建立線程的本質和上文中是同樣的。因此不能簡單的認爲線程池也是一種新的建立線程的方式。

    線程池建立線程代碼示例

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
    * <p>
    * 線程池建立線程的方法
    * </p>
    *
    * @author 踏雪彡尋梅
    * @version 1.0
    * @date 2020/9/7 - 23:05
    * @since JDK1.8
    */
    public class ThreadPools {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 1000; i++) {
                // 添加任務
                executorService.submit(new Task() {});
            }
        }
    }
    
    class Task implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }

    線程池建立線程源碼(DefaultThreadFactory 中)

  • 分析

2.」經過 Callable 和 FutureTask 建立線程,也算是一種新建線程的方式。「

  1. 從類圖中能夠知道這兩個建立線程的本質也是和以前的同樣的。

    類圖展現

  • 分析

3.」無返回值是實現 Runnable 接口,有返回值是實現 Callable 接口,因此 Callable 是新的實現線程的方式。「

    • 和第 2 點差很少。

    4.定時器是新的實現線程的方式。

    1. 和前面幾點同樣,定時器建立線程的方法最終本質也離不開上文中的那兩類方法。

      定時器實現線程代碼示例

      import java.util.Timer;
      import java.util.TimerTask;
      
      /**
      * <p>
      * 定時器建立線程
      * </p>
      *
      * @author 踏雪彡尋梅
      * @version 1.0
      * @date 2020/9/7 - 23:48
      * @since JDK1.8
      */
      public class DemoTimmerTask {
          public static void main(String[] args) {
              Timer timer = new Timer();
              timer.scheduleAtFixedRate(new TimerTask() {
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName());
                  }
              }, 1000, 1000);
          }
      }
    • 分析

    5.匿名內部類和 Lambda 表達式的方式建立線程是新的建立線程方式。

    1. 實際上也和前面幾點同樣是一個表面現象,本質上仍是那兩種方法。

      使用方式代碼示例

      /**
      * <p>
      * 匿名內部類建立線程
      * </p>
      *
      * @author 踏雪彡尋梅
      * @version 1.0
      * @date 2020/9/7 - 23:54
      * @since JDK1.8
      */
      public class AnonymousInnerClassDemo {
          public static void main(String[] args) {
              // 第一種
              new Thread() {
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName());
                  }
              }.start();
      
              // 第二種
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName());
                  }
              }).start();
          }
      }
      /**
      * <p>
      * Lambda 表達式建立線程
      * </p>
      *
      * @author 踏雪彡尋梅
      * @version 1.0
      * @date 2020/9/7 - 23:58
      * @since JDK1.8
      */
      public class LambdaDemo {
          public static void main(String[] args) {
              new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
          }
      }

      運行結果

    • 總結

      • 多線程的實現方式,在代碼中寫法變幻無窮,但其本質萬變不離其宗。

    常見面試問題

    • 有多少種實現線程的方法?思路有 5 點。

      • 從不一樣的角度看,會有不一樣的答案。

      • 典型答案是兩種。這兩種方式的對比。

      • 從原理來看,兩種本質都是同樣的(都是實現 run 方法)。

      • 具體展開說其餘方式(代碼的實現上的不一樣方式,原理仍是基於那兩個本質)。

      • 將以上幾點作結論。

    • 實現 Runnable 接口和繼承 Thread 類哪一種方式更好?

      • 從代碼架構角度。(應該去解耦,兩件事情:1.具體的任務即 run 方法中的內容;2.和線程生命週期相關的如建立線程、運行線程、銷燬線程即 Thread 類去作的事情)

      • 新建線程的損耗的角度。(繼承 Thread 類,須要新建線程、執行完以後還要銷燬,實現 Runnable 接口的方式能夠反覆的利用同一個線程,好比線程池就是這麼作的,用於線程生命週期的損耗就減小了)

      • Java 不支持多繼承的角度。(對於擴展性而言)

    若有寫的不足的,請見諒,請你們多多指教。

    - END -

    ▐往期推薦 

    若是你以爲文章不錯,文末的贊 ???? 又回來啦,記得給我「點贊」和「在看」哦~