多執行緒入門

  • 2019 年 10 月 3 日
  • 筆記

什麼是進程和執行緒?

一條執行緒指的是進程中一個單一順序的控制流,一個進程中可以並發多個執行緒,每條執行緒並行執行不同的任務。

多執行緒是多任務的一種特別的形式,但多執行緒使用了更小的資源開銷。

一個進程包括由作業系統分配的記憶體空間,包含一個或多個執行緒。一個執行緒不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到所有的非守護執行緒都結束運行後才能結束。

為什麼使用多執行緒?

使用多執行緒可以編寫高效率的程式來達到充分利用 CPU,可以大大提高系統整體的並發能力以及性能.

執行緒的生命周期

執行緒是一個動態執行的過程,它也有一個從產生到死亡的過程。

下圖顯示了一個執行緒完整的生命周期。

  • 新建狀態:
    使用 new 關鍵字和 Thread 類或其子類建立一個執行緒對象後,該執行緒對象就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒.
  • 就緒狀態:
    當執行緒對象調用了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒隊列中,要等待JVM里執行緒調度器的調度。
  • 運行狀態:
    如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於運行狀態。處於運行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
  • 阻塞狀態:
    如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分為三種:
    • 等待阻塞:運行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
    • 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
    • 其他阻塞:通過調用執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
  • 死亡狀態:
    一個運行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

創建執行緒的方式

java 提供了三種創建執行緒的方法:實現 Runnable 介面;繼承 Thread 類本身;通過 Callable 和 Future 創建執行緒。

1.實現 Runnable 介面

創建一個執行緒,最簡單的方法是創建一個實現 Runnable 介面的類,同時重寫 run()方法.

然後創建Runnable實現類的實例,並以此實例作為Thread的target對象,即該Thread對象才是真正的執行緒對象。

新執行緒創建之後,你調用它的 start() 方法它才會運行。

package com.example.test;    /**   * @author ydx   */  public class RunnableDemo implements Runnable{        /**       * 執行緒名稱       */      private String threadName;        /**       * 構造方法       * @param threadName 執行緒名稱       */      public RunnableDemo(String threadName) {          this.threadName = threadName;      }        @Override      public void run() {          System.out.println(threadName + " is running");          //業務          for (int i = 0; i < 5; i++) {              System.out.println(threadName + " 執行 " + i);              try {                  Thread.sleep(100);              } catch (InterruptedException e) {                  e.printStackTrace();              }          }          System.out.println(threadName + " is exiting");      }        public static void main(String[] args) {            RunnableDemo runnable1 = new RunnableDemo("thread-1");          Thread thread1 = new Thread(runnable1);          thread1.start();            RunnableDemo runnable2 = new RunnableDemo("thread-2");          Thread thread2 = new Thread(runnable2);          thread2.start();        }  }  

第一次運行結果如下:

thread-1 is running  thread-1 執行 0  thread-2 is running  thread-2 執行 0  thread-2 執行 1  thread-1 執行 1  thread-2 執行 2  thread-1 執行 2  thread-1 執行 3  thread-2 執行 3  thread-1 執行 4  thread-2 執行 4  thread-2 is exiting  thread-1 is exiting  

第二次運行結果如下:

thread-1 is running  thread-1 執行 0  thread-2 is running  thread-2 執行 0  thread-1 執行 1  thread-2 執行 1  thread-1 執行 2  thread-2 執行 2  thread-1 執行 3  thread-2 執行 3  thread-2 執行 4  thread-1 執行 4  thread-1 is exiting  thread-2 is exiting  

可以看出兩次運行結果是不一樣的,每次兩個執行緒的執行順序是隨機的.

2.繼承Thread類

創建一個執行緒的第二種方法是創建一個新的類,該類繼承 Thread 類,然後創建一個該類的實例。

繼承類必須重寫 run() 方法,該方法是新執行緒的入口點。它也必須調用 start() 方法才能執行。

package com.example.test;    /**   * @author ydx   */  public class ThreadDemo extends Thread {        /**       * 執行緒名稱       */      private String threadName;        public ThreadDemo(String threadName) {          this.threadName = threadName;      }        @Override      public synchronized void start() {          System.out.println(threadName+ " is starting......");          super.start();      }        @Override      public void run() {          System.out.println(threadName + " is running");          //業務          for (int i = 0; i < 3; i++) {              System.out.println(threadName + " 執行 " + i);              try {                  Thread.sleep(100);              } catch (InterruptedException e) {                  e.printStackTrace();              }          }          System.out.println(threadName + " is exiting");      }        public static void main(String[] args) {          ThreadDemo thread1 = new ThreadDemo("thread-1");          thread1.start();            ThreadDemo thread2 = new ThreadDemo("thread-2");          thread2.start();        }  }  

運行結果如下:

thread-1 is starting......  thread-2 is starting......  thread-1 is running  thread-1 執行 0  thread-2 is running  thread-2 執行 0  thread-1 執行 1  thread-2 執行 1  thread-2 執行 2  thread-1 執行 2  thread-2 is exiting  thread-1 is exiting  

3.通過 Callable 和 Future 創建執行緒

  1. 創建 Callable 介面的實現類,並實現 call() 方法,該 call() 方法將作為執行緒執行體,並且有返回值。
  2. 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
  3. 使用 FutureTask 對象作為 Thread 對象的 target 創建並啟動新執行緒。
  4. 調用 FutureTask 對象的 get() 方法來獲得子執行緒執行結束後的返回值。

    package com.example.test;    import java.util.concurrent.Callable;  import java.util.concurrent.ExecutionException;  import java.util.concurrent.FutureTask;    /**   * @author ydx   */  public class CallableTest implements Callable<Integer> {        @Override      public Integer call() throws Exception {          int sum = 0;          for (int i = 0; i < 5; i++) {              sum += i;              System.out.println(i);              //sleep 200ms              Thread.sleep(200);          }          return sum;      }        public static void main(String[] args) {            long start = System.currentTimeMillis();            CallableTest callableTest = new CallableTest();          FutureTask<Integer> futureTask = new FutureTask<>(callableTest);          new Thread(futureTask, "thread-1").start();            CallableTest callableTest2 = new CallableTest();          FutureTask<Integer> futureTask2 = new FutureTask<>(callableTest2);          new Thread(futureTask2, "thread-2").start();          try {              System.out.println("thread-1的結果: " + futureTask.get());              System.out.println("thread-2的結果: " + futureTask.get());          } catch (InterruptedException e) {              e.printStackTrace();          } catch (ExecutionException e) {              e.printStackTrace();          }            long end = System.currentTimeMillis();          System.out.println("耗時: " + (end - start) + "ms");      }  }  

    運行結果:

    0  0  1  1  2  2  3  3  4  4  thread-1的結果: 10  thread-2的結果: 10  耗時: 1004ms  

    我們創建了兩個執行緒, 每個執行緒計算0~4的和,單個執行緒耗時200ms * 5 = 1000ms,而最終兩個執行緒的總耗時約1000ms,由此可見兩個執行緒是並發進行.

4.創建執行緒的三種方式的對比

  • 使用繼承 Thread 類的方式創建多執行緒時,編寫簡單,但是不夠靈活
  • 採用實現 Runnable、Callable 介面的方式創建多執行緒時,執行緒類只是實現了 Runnable 介面或 Callable 介面,還可以繼承其他類,創建執行緒比較靈活.

執行緒管理

Java提供了一些方法用於執行緒狀態的控制。具體如下:

1.sleep(執行緒睡眠)

如果我們需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過調用Thread的sleep方法。

Thread.sleep(long millis)方法,millis參數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。

2.wait(執行緒等待)

Object類中的wait()方法,導致當前的執行緒等待,直到其他執行緒調用此對象的 notify() 方法或 notifyAll() 喚醒方法.

3.yield(執行緒讓步)

Thread.yield() 方法,暫停當前正在執行的執行緒對象,把執行機會讓給相同或者更高優先順序的執行緒。

和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。

yield()方法只是讓當前執行緒暫停一下,重新進入就緒的執行緒池中,讓系統的執行緒調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個執行緒調用yield()方法之後,執行緒調度器又將其調度出來重新進入到運行狀態執行。

4.join(執行緒加入)

join()方法,等待其他執行緒終止。在當前執行緒中調用另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個進程運行結束,當前執行緒再由阻塞轉為就緒狀態。

5.notify(執行緒喚醒)

Object類中的notify()方法,喚醒在此對象監視器上等待的單個執行緒。如果所有執行緒都在此對象上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的執行緒放棄此對象上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該對象上主動同步的其他所有執行緒進行競爭

執行緒的優先順序

每一個 Java 執行緒都有一個優先順序,這樣有助於作業系統確定執行緒的調度順序。

Java 執行緒的優先順序是一個整數,其取值範圍是 1 (Thread.MINPRIORITY ) – 10 (Thread.MAXPRIORITY )。

默認情況下,每一個執行緒都會分配一個優先順序 NORM_PRIORITY(5)。

具有較高優先順序的執行緒對程式更重要,並且應該在低優先順序的執行緒之前分配處理器資源。但是,執行緒優先順序不能保證執行緒執行的順序,而且非常依賴於平台。

執行緒池

執行緒池,其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁創建執行緒對象的操作,無需反覆創建執行緒而消耗過多資源。使用執行緒池的好處:

  • 降低資源消耗。通過重複利用已創建的執行緒降低執行緒創建和銷毀造成的消耗.
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒創建就能立即執行。
  • 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

Java通過Executors提供四種執行緒池,分別為:

  1. newCachedThreadPoo 創建一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閑執行緒,若無可回收,則新建執行緒。
  2. newFixedThreadPool 創建一個定長執行緒池,可控制執行緒最大並發數,超出的執行緒會在隊列中等待。
  3. newScheduledThreadPool 創建一個定長執行緒池,支援定時及周期性任務執行。
  4. newSingleThreadExecutor 創建一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。