執行緒以及多執行緒開發

  • 2019 年 11 月 8 日
  • 筆記

進程和執行緒

在學習執行緒之前,首先要理解什麼是進程。打開你的任務管理器,導航欄第一個清清楚楚的寫著進程,點進去會發現是許許多多的你在運行的程式,這就是一個進程。

like this:

現代作業系統都可以同時執行多個程式,這就是多任務。執行緒時建立在進程的基礎上的,比如QQ音樂這個進程可以同時在執行播放、下載、傳輸等動作。這就叫多執行緒,每個執行緒在執行不同的功能。
在單核CPU系統中,也可以同時運行多個程式,程式運行是搶佔式的,QQ運行0.001S,chrome運行0.01s,這個時間人是感知不出來的,我們就會覺得在同時執行。所以為了提高效率,現在的手機、電腦都是非常多核的。

進程和執行緒的關係就是:一個進程可以包含一個或多個執行緒,但至少會有一個執行緒。

作業系統調度的最小任務單位其實不是進程,而是執行緒。

進程 VS 執行緒

進程和執行緒是包含關係,但是多任務既可以由多進程實現,也可以由執行緒實現,還可以混合多進程+多執行緒。

和多執行緒相比,多進程的缺點是:

  • 創建進程比創建執行緒開銷大很多,尤其是在Windows上
  • 進程間通訊比執行緒要慢,因為執行緒見通訊就是讀寫同一個變數,速度很快

多進程的優點:

  • 多進程穩定性比多執行緒高,因為在多進程情況下,一個進程的崩潰不會影響其他進程,任何一個執行緒崩潰會導致整個進程崩潰。

創建執行緒

1. Thread

例:

public class MyThread extends Thread {  // 執行緒的主體類      @Override      public void run(){         System.out.println("Thread is starting");      }  }  

上面的MyThread類繼承Thread,覆寫了run方法。一個類只要繼承了此類,就表示這個類為執行緒的主體類。run()是執行緒的主方法,多執行緒要執行的方法都在這寫。
但是run()方法是不能被直接調用的,這牽涉到系統的資源調度問題,想要啟動多執行緒,必須用start()完成。

調用執行緒
public class ThreadDemo {      public static void main(String[] args) {       new MyThread().start();          // 啟動新執行緒  }

java語言內置了多執行緒支援。當Java程式啟動的時候其實是啟動了一個JVM進程。JVM啟動主執行緒來執行main()方法,在main()方法中可以啟動其他執行緒。

start() 只能由 Thread類型實例調用,表示啟動一個執行緒。

執行結果
"C:Program FilesJavajdk1.8.0_221binjava.exe"    Thread is starting

由此可見,執行緒創建成功

那麼創建一個多執行緒呢?

創建多執行緒
// 多執行緒主體類  public class MyThread extends Thread {      private String title;      public MyThread(){      }      MyThread(String title){          this.title = title;      }      @Override      public void run(){          for (int i = 0; i<10;i++){              System.out.println(this.title +  "is starting");              System.out.println(Thread.currentThread().getName());          }      }  }         public static void main(String[] args) {          new Thread(new MyThread("A"),"執行緒1").start();          new Thread(new MyThread("C"),"執行緒2").start();          new Thread(new MyThread("B")).start();      }

執行結果:

這個結果中有幾個關注點:

  1. 多執行緒的執行是無序的,不可控的
  2. 調用的是start()方法,但執行的是run()方法

我們來看一下源碼,分析一下

public synchronized void start() {            if (threadStatus != 0)  // 判斷執行緒狀態            // 每一個執行緒的類的對象只允許啟動一次,重複啟動就會拋出這個異常,由run()拋出              throw new IllegalThreadStateException();            group.add(this);            boolean started = false;          try {          // 調用此方法              start0();              started = true;          } finally {              try {                  if (!started) {                      group.threadStartFailed(this);                  }              } catch (Throwable ignore) {                }          }      }        private native void start0();       // 注釋部分被我刪掉了,太長了 

我們發現start()方法調用的是start0(),而start0()並沒有實現,還被native修飾,那麼native是啥呢?

在Java程式執行的過程中考慮到對於不同層次的開發者需求,支援了本地的作業系統函數調用。這項技術被稱為JNI(Java Native Interface),但在Java開發過程中並不推薦這樣使用。利用這項技術,可以利用作業系統提供的的底層函數,操作一些特殊的處理。

不同的系統在進行資源調度的時候由自己的一套演算法,要想調用start()方法啟動執行緒,就要實現start0(),這時候JVM就會根據不同的作業系統來具體實現start0(),總結就是一切的一切都是跨平台帶來的。

這也規定了,啟動多執行緒只有一種方案,調用Thread類中的start()方法.

  1. Thread 構造函數可以接收一個實例對象和執行緒的名字參數。

Thread.currentThread().getName() 就代表了獲取當前執行緒的名字。

在返回值中還出現了"Thread-3",這是由於Thread會自動給沒有命名的執行緒分配一個不會重複的名字。

這種方式啟動多執行緒固然沒錯,但存在單繼承的隱患。下面就給出另一種模式。

2. Runnable

首先分別來看一下Thread類的實現

public  class Thread implements Runnable {}

原來Thread是繼承了Runnable

再看一下Runnable介面

@FunctionalInterface  public interface Runnable {      /**       * When an object implementing interface <code>Runnable</code> is used       * to create a thread, starting the thread causes the object's       * <code>run</code> method to be called in that separately executing       * thread.       * <p>       * The general contract of the method <code>run</code> is that it may       * take any action whatsoever.       *       * @see     java.lang.Thread#run()       */      public abstract void run();  }

再次驚訝,原來這個run方法也是從這裡繼承的。

那就清楚了,來試一下吧。

// 只需要實現 Runnable,重寫run()即可,其他絲毫未變  public class MyThread implements Runnable {      private String title;      public MyThread(){      }      MyThread(String title){          this.title = title;      }      @Override      public void run(){          for (int i = 0; i<10;i++){              System.out.println(this.title +  "is starting");              System.out.println(Thread.currentThread().getName());          }      }  }          public class ThreadDemo {      public static void main(String[] args) {          new Thread(new MyThread("A執行緒"),"執行緒1").start();          new Thread(new MyThread("C執行緒"),"執行緒2").start();          new Thread(new MyThread("B執行緒")).start();         // lambda 語法實現  //        new Thread(() -> {  //           System.out.println("啟動新的執行緒");  //       }).start();          }  }

結果:

完全一致。

在以後的多執行緒設計實現,優先使用Runnable介面實現。

還沒完,我們依靠Runnable介面實現的時候,會發現有一個缺陷,就是沒有返回值,那有沒有帶返回值的實現方式呢?有!繼續看

3. Callable

在Java1.5之後為了解決run()方法沒有返回值的問題,引入了新的執行緒實現java.util.concurrent.Callable介面.

我們看一下Oracle的api文檔:

可以看到Callable定義的時候利用了一個泛型,代表了返回數據的類型,避免了向下轉型帶來的安全隱患

了解向下轉型可以看我的另一篇文章:https://www.cnblogs.com/gyyyblog/p/11806601.html

但是問題又來了,我們上面已經說過了,要想啟動多執行緒,必須使用Thread類提供的
start() 方法調用Runnable介面的 run() 方法,可是現在 Callable中並沒有run() 方法,那怎麼辦呢?

再來找到一個FutureTask類:

public class FutureTask<V>  extends Object  implements RunnableFuture<V>

構造方法:

它的構造方法可以接收一個Callable類型參數

它又繼承了RunnableFuture<V>,那就繼續往上找

public interface RunnableFuture<V>  extends Runnable, Future<V>

出現了,它出現了,Runnable我們知道了,是沒有返回值的,現在看看Future<V>是個啥

它有一個get()方法可以得到一個泛型返回值。

OK,現在我們就可以梳理一下找到的這些東西怎麼個關係:

具體實現

public class CallableThread implements Callable<String> {  // 繼承實現Callable<V>      // 覆寫call()方法      @Override      public String call() throws Exception{          for (int i = 0;i<10;i++){              System.out.println("*********執行緒執行、i="+ i);          }          return "執行緒執行完畢";      }  }        // 調用          FutureTask<String> task = new FutureTask<>(new CallableThread());          new Thread(task).start();          System.out.println("【執行緒返回數據】" + task.get());

結果:

為了得到一個返回值可真不容易,核心思想還是實例化一個Thread對象,可通過其構造方法接收一個Rannable類型參數,調用start()啟動執行緒.

總結:基本來說就這三種創建多執行緒模式,根據場景使用。

**純屬個人理解,希望指正錯誤,共同交流。

Tags: