《Java 高階篇》七:執行緒和執行緒池

ACatSmiling發表於2024-10-02

Author: ACatSmiling

Since: 2024-10-01

程式、程序和執行緒

程式(program):是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼,靜態物件。

程序(process):是程式的一次執行過程,或是正在執行的一個程式。是一個動態的過程:有它自身的產生、存在和消亡的過程 —— 生命週期。如:執行中的 QQ,執行中的 MP3 播放器。

  • 程式是靜態的,程序是動態的。

  • 程序作為資源分配的單位,系統在執行時會為每個程序分配不同的記憶體區域。

執行緒(thread):程序可進一步細化為執行緒,是一個程式內部的一條執行路徑。

  • 若一個程序同一時間並行執行多個執行緒,就是支援多執行緒的。
  • 執行緒作為排程和執行的單位,每個執行緒擁有獨立的執行棧和程式計數器(pc),執行緒切換的開銷小。
  • 一個程序中的多個執行緒共享相同的記憶體單元/記憶體地址空間(方法區、堆):它們從同一堆中分配物件,可以訪問相同的變數和物件。這就使得執行緒間通訊更簡便、高效。但多個執行緒操作共享的系統資源可能就會帶來安全的隱患。

單核 CPU 和多核 CPU 的理解

  • 單核 CPU,其實是一種假的多執行緒,因為在一個時間單元內,也只能執行一個執行緒的任務。只是因為 CPU 時間單元特別短,因此感覺不出來。例如:雖然有多車道,但是收費站只有一個工作人員在收費,只有收了費才能透過,那麼 CPU 就好比收費人員。如果有某個人沒準備好交錢,那麼收費人員可以把他 "掛起",晾著他,等他準備好了錢,再去收費。

  • 如果是多核的話,才能更好的發揮多執行緒的效率,現在的伺服器基本都是多核的。

  • 一個 Java 應用程式 java.exe,其實至少有三個執行緒:main() 主執行緒gc() 垃圾回收執行緒異常處理執行緒。當然如果發生異常,會影響主執行緒。

並行與併發

  • 並行:多個 CPU 同時執行多個任務。 比如:多個人同時做不同的事。
  • 併發:一個 CPU(採用時間片)同時執行多個任務。比如:秒殺、多個人做同一件事。

多執行緒程式的優點:

以單核 CPU 為例,只使用單個執行緒先後完成多個任務(呼叫多個方法),肯定比用多個執行緒來完成用的時間更短(因為單核 CPU,在多執行緒之間進行切換時,也需要花費時間),為何仍需多執行緒呢?

  • 提高應用程式的響應。對圖形化介面更有意義,可增強使用者體驗。
  • 提高計算機系統 CPU 的利用率。
  • 改善程式結構。將既長又複雜的程序分為多個執行緒,獨立執行,利於理解和修改。

何時需要多執行緒:

  • 程式需要同時執行兩個或多個任務。
  • 程式需要實現一些需要等待的任務時,如使用者輸入、檔案讀寫操作、網路操作、搜尋等。
  • 需要一些後臺執行的程式時。

Thread 類

Java 語言的 JVM 允許程式執行多個執行緒,它透過java.lang.Thread類來體現。

Thread 類的特性:

  • 每個執行緒都是透過某個特定 Thread 物件的run()方法來完成操作的,經常把 run() 方法的主體稱為執行緒體
  • 應該透過 Thread 物件的start()方法來啟動這個執行緒,而非直接呼叫 run()。
    • 如果手動呼叫 run(),那麼就只是普通的方法,並沒有啟動多執行緒。
    • 呼叫 start() 之後,run() 由 JVM 呼叫,什麼時候呼叫以及執行的過程控制都由作業系統的 CPU 排程決定。

構造器:

  • Thread()

  • Thread(String threadname)

  • Thread(Runnable target)

  • Thread(Runnable target, String name)

方法:

  • void start():啟動當前執行緒,並執行當前執行緒物件的run()方法。

  • run():通常需要重寫 Thread 類中的此方法,將建立的執行緒在被排程時需要執行的操作宣告在此方法中。

  • static Thread currentThread():靜態方法,返回執行當前程式碼的執行緒。在 Thread 子類中就是 this,通常用於主執行緒和 Runnable 實現類。

  • String getName():返回當前執行緒的名稱。

  • void setName(String name):設定當前執行緒的名稱。

    public class Test {
        public static void main(String[] args) {
            // 設定 main 執行緒的名字
            Thread.currentThread().setName("主執行緒");
            System.out.println(Thread.currentThread().getName());
    
            // 設定自定義執行緒的名字
            MyThread myThread = new MyThread();
            myThread.setName("自定義執行緒一");
            myThread.start();
    
            // 構造器設定自定義執行緒的名字
            new MyThread("自定義執行緒二").start();
        }
    }
    
    class MyThread extends Thread {
        public MyThread() {
    
        }
    
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
    
  • static void yield():釋放當前執行緒 CPU 的執行權,但有可能 CPU 再次分配資源時,仍然優先分配到當前執行緒。

  • join():在某個執行緒 a 中呼叫執行緒 b 的 join() 方法時,呼叫執行緒 a 將進入阻塞狀態,直到執行緒 b 執行完之後,執行緒 a 才結束阻塞狀態,然後重新排隊等待 CPU 分配資源執行剩下的任務。注意:呼叫 join() 方法之後,比當前執行緒低優先順序的執行緒也可以獲得執行。

  • static void sleep(long millis):讓當前執行緒 "睡眠" 指定的 millis 毫秒時間,在指定的 millis 毫秒時間內,當前執行緒是阻塞狀態。時間到達時,重新排隊等待 CPU 分配資源。

  • stop():強制結束當前執行緒,已過時。

  • boolean isAlive():返回 boolean,判斷執行緒是否存活。

示例:

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        System.out.println(myThread.isAlive());

        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            if (i == 20) {
                try {
                    myThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ;
        }

        System.out.println(myThread.isAlive());
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            if (i % 20 == 0) {
                yield();
            }

            if (i % 30 == 0) {
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行緒

執行緒的排程策略

排程策略:

  • 時間片策略

    image-20210306161555130

  • 搶佔式策略:高優先順序的執行緒搶佔 CPU。

Java 的排程方法:

  • 同優先順序執行緒,組成先進先出佇列(先到先服務),使用時間片策略。

  • 高優先順序執行緒,使用優先排程的搶佔式策略。

執行緒的優先順序

執行緒的優先順序等級:

  • MAX_PRIORITY:10,最大優先順序。
  • MIN _PRIORITY:1,最小優先順序。
  • NORM_PRIORITY:5,預設優先順序。

涉及的方法:

  • getPriority():獲取執行緒的優先值。

  • setPriority(int newPriority):設定執行緒的優先順序。

    System.out.println(Thread.currentThread().getPriority());
    Thread.currentThread().setPriority(8);
    

說明:

  • 執行緒建立時繼承父執行緒的優先順序。

  • 低優先順序只是獲得排程的機率低,但並非一定是在高優先順序執行緒之後才被呼叫。

執行緒的分類

Java 中的執行緒分為兩類:一種是使用者執行緒,一種是守護執行緒

  • 使用者執行緒和守護執行緒,幾乎在每個方面都是相同的,唯一的區別是判斷 JVM 何時離開。

  • 守護執行緒是用來服務使用者執行緒的,透過在 start() 前呼叫thread.setDaemon(true),可以把一個使用者執行緒變成一個守護執行緒。

  • Java 垃圾回收就是一個典型的守護執行緒。

  • 若 JVM 中都是守護執行緒,當前 JVM 將退出。

執行緒的生命週期

要想實現多執行緒,必須在主執行緒中建立新的執行緒物件。Java 語言使用 Thread 類及其子類的物件來表示執行緒,並用Thread.State類定義了執行緒的幾種狀態,在它的一個完整的生命週期中通常要經歷如下的五種狀態:

image-20210307170916324

  • 新建:當一個 Thread 類或其子類的物件被宣告並建立時,新生的執行緒物件處於新建狀態。
  • 就緒:處於新建狀態的執行緒被 start() 後,將進入執行緒佇列等待 CPU 時間片,此時它已具備了執行的條件,只是沒分配到 CPU 資源。
  • 執行:當就緒的執行緒被排程並獲得 CPU 資源時,便進入執行狀態,run() 定義了執行緒的操作和功能。
  • 阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態。
  • 死亡:執行緒完成了它的全部工作或執行緒被提前強制性地中止或出現異常導致結束。

執行緒的建立

執行緒建立的一般過程:

image-20210310100001907

方式一:繼承 Thread 類

  • 建立一個繼承 Thread 類的子類。

  • 重寫 Thread 類的 run():將此執行緒執行的操作宣告在 run() 方法體中。

  • 建立 Thread 類的子類的物件例項。

  • 透過該物件呼叫 start()。

    • 啟動當前執行緒。
    • 呼叫當前執行緒的 run()。
    • 不能透過直接呼叫物件的 run() 的形式啟動執行緒。
    • 不能再次呼叫當前物件的 start() 去開啟一個新的執行緒,否則報 java.lang.IllegalThreadStateException 異常 。
    • 如果要啟動一個新的執行緒,需要重新建立一個 Thread 類的子類的物件,並呼叫其 start()。

示例一:

public class Test {
  public static void main(String[] args) {
      // 啟動一個子執行緒
      MyThread myThread = new MyThread();
      myThread.start();
      
      // 啟動一個新的子執行緒,並執行 run 方法
      MyThread myThread2 = new MyThread();
      myThread2.start();
      
      // main 執行緒
      for (int i = 0; i <= 100; i++) {
          if (i % 2 != 0) {
              System.out.println(Thread.currentThread().getName() + ":" + i);
          }
      }
  }
}

class MyThread extends Thread {
  @Override
  public void run() {
      for (int i = 0; i <= 100; i++) {
          if (i % 2 == 0) {
              System.out.println(Thread.currentThread().getName() + ":" + i);
          }
      }
  }
}

示例二:

public class Test {
    public static void main(String[] args) {
        // 匿名內部類
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}

方式二:實現 Runnable 介面

  • 建立一個實現了 Runnable 介面的類。

  • 實現類去實現 Runnable 介面中的抽象方法:run()。

  • 建立實現類的物件。

  • 將此物件作為引數傳遞到 Thread 類的構造器中,然後建立 Thread 類的物件。

  • 透過 Thread 類的物件,呼叫 start(),最終執行的是上面重寫的 run()。

示例:

public class Test {
  public static void main(String[] args) {
      // 啟動多個子執行緒時,只需要建立一個 Runnable 介面實現類的物件
      MyRunnable myRunnable = new MyRunnable();
      
      // 啟動一個子執行緒
      Thread thread = new Thread(myRunnable);
      thread.start();

      // 啟動一個新的子執行緒,並執行 run 方法
      Thread thread2 = new Thread(myRunnable);
      thread2.start();


      // main 執行緒
      for (int i = 0; i <= 100; i++) { 
          if (i % 2 != 0) {
              System.out.println(Thread.currentThread().getName() + ":" + i);
          }
      }
  }
}

class MyRunnable implements Runnable {
  @Override
  public void run() {
      for (int i = 0; i <= 100; i++) {
          if (i % 2 == 0) {
              System.out.println(Thread.currentThread().getName() + ":" + i);
          }
      }
  }
}

方式一和方式二的對比

開發中,優先選擇實現 Runnable 介面的方式

  • 實現 Runnable 介面的方式,沒有類的單繼承性的侷限性。

  • 實現 Runnable 介面的方式,更適合處理多個執行緒有共享資料的情況。

  • Thread 類也實現了 Runnable 介面,無論是方式一,還是方式二,都需要重寫 Runnable 介面的 run() 方法,並將建立的執行緒需要執行的邏輯宣告在 run() 方法中。

方式三:實現 Callable 介面

  • 從 JDK 5.0 開始。

  • 建立一個實現 Callable 介面的實現類。

  • 實現call(),將此執行緒需要執行的操作宣告在 call() 的方法體中。

  • 建立 Callable 介面實現類的物件。

  • 將此 Callable 介面實現類的物件作為引數傳遞到 FutureTask 的構造器中,建立 FutureTask 的物件。

    • Future 介面可以對具體 Runnable 或 Callable 任務的執行結果進行取消、查詢是否完成、獲取結果等操作。
    • FutrueTask 是 Futrue 介面的唯一的實現類。
    • FutureTask 同時實現了 Runnable 和 Future 介面。它既可以作為 Runnable 被執行緒執行,又可以作為 Future 得到 Callable 的返回值。
      • Runnable 介面的 run() 沒有返回值。
      • Callable 介面的 call() 有返回值。
  • 將 FutureTask 的物件作為引數傳遞到 Thread 類的構造器中,建立 Thread 類的物件,並呼叫 start(),啟動執行緒。

  • 根據實際需求,選擇是否獲得 Callable 中 call() 的返回值。

示例:

public class Test {
  public static void main(String[] args) {
      // 3. 建立 Callable 介面實現類的物件
      MyCallable myCallable = new MyCallable();
      // 4. 將此 Callable 介面實現類的物件作為引數傳遞到 FutureTask 的構造器中,建立 FutureTask 的物件
      FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
      // 5. 將 FutureTask 的物件作為引數傳遞到 Thread 類的構造器中,建立 Thread 類的物件,並呼叫 start(),啟動執行緒
      new Thread(futureTask).start();
      // 6. 獲得 Callable 中 call() 的返回值
      try {
          // get() 返回值即為 FutureTask 構造器引數 Callable 實現類重寫的 call() 的返回值
          Integer sum = futureTask.get();
          System.out.println("100以內偶數的總和為:" + sum);
      } catch (InterruptedException | ExecutionException e) {
          e.printStackTrace();
      }
  }
}

// 1. 建立一個實現 Callable 介面的實現類
class MyCallable implements Callable<Integer> {
  // 2. 實現 call() 方法,將此執行緒需要執行的操作宣告在 call() 的方法體中
  @Override
  public Integer call() throws Exception {
      int sum = 0;
      for (int i = 1; i <= 100; i++) {
          if (i % 2 == 0) {
              sum += i;
          }
      }
      return sum;
  }
}

與使用 Runnable 介面相比,Callable 介面功能更強大些:

  • 相比 run() 方法,call() 可以有返回值。
  • call() 可以丟擲異常,能夠被外面的操作捕獲,獲取異常的資訊。
  • Callable 支援泛型的返回值。
  • Callable 需要藉助 FutureTask 類,比如獲取 call() 的返回結果。

方式四:執行緒池

背景: 經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,會對效能影響很大。

思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中,這樣可以避免頻繁建立銷燬,實現重複利用。類似生活中的公共交通工具。

好處:

  • 提高響應速度,減少了建立新執行緒的時間。
  • 降低資源消耗,重複利用執行緒池中執行緒,不需要每次都建立。
  • 便於執行緒管理。
    • corePoolSize:核心池的大小。
    • maximumPoolSize:最大執行緒數。
    • keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止。

JDK 5.0 起,提供了執行緒池相關 API:ExecutorServiceExecutors

  • ExecutorService:真正的執行緒池介面,常用子類ThreadPoolExecutor

    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行 Runnable。
    • <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一般用來執行 Callable。
    • void shutdown():關閉連線池。
  • Executors:工具類、執行緒池的工廠類,用於建立並返回不同型別的執行緒池。

    • Executors.newCachedThreadPool():建立一個可根據需要建立新執行緒的執行緒池。

    • Executors.newFixedThreadPool(n):建立一個可重用固定執行緒數的執行緒池。

    • Executors.newSingleThreadExecutor():建立一個只有一個執行緒的執行緒池。

    • Executors.newScheduledThreadPool(n):建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。

示例:

public class ThreadPool {
    public static void main(String[] args) {
        // 1. 提供指定執行緒數量的執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 2. 執行指定的執行緒的操作,需要提供實現 Runnable 介面或 Callable 介面的實現類的物件

        // 2-1. execute() 適合使用於 Runnable
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + i);
                    }
                }
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + i);
                    }
                }
            }
        });

        // 2-2. submit() 適合適用於 Callable
        Future<Integer> evenSum = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int evenSum = 0;
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 == 0) {
                        evenSum += i;
                    }
                }
                return evenSum;
            }
        });
        try {
            System.out.println("100以內的偶數和: " + evenSum.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        Future<Integer> oddSum = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int oddSum = 0;
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 != 0) {
                        oddSum += i;
                    }
                }
                return oddSum;
            }
        });
        try {
            System.out.println("100以內的奇數和: " + oddSum.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 3. 使用完執行緒池後,需要關閉執行緒池
        executorService.shutdown();
    }
}

執行緒的同步

執行緒的安全問題

多執行緒安全問題例項,模擬火車站售票程式,開啟三個視窗售票。

方式一:繼承 Thread 類。

public class TestThread {
    public static void main(String[] args) {
        // 啟動第一個售票視窗
        TicketThread thread1 = new TicketThread();
        thread1.setName("售票視窗一");
        thread1.start();

        // 啟動第二個售票視窗
        TicketThread thread2 = new TicketThread();
        thread2.setName("售票視窗二");
        thread2.start();

        // 啟動第三個售票視窗
        TicketThread thread3 = new TicketThread();
        thread3.setName("售票視窗三");
        thread3.start();

    }
}

class TicketThread extends Thread {
    // 總票數,必須定義為 static,隨類只載入一次,因為每新建一個執行緒,都需要 new 一次 TicketThread
    private static int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
            } else {
                break;
            }
        }
    }
}

方式二:實現 Runnable 介面。

public class Test {
    public static void main(String[] args) {
        TicketRunnable ticket = new TicketRunnable();

        // 啟動第一個售票視窗
        Thread thread1 = new Thread(ticket, "售票視窗1");
        thread1.start();

        // 啟動第二個售票視窗
        Thread thread2 = new Thread(ticket, "售票視窗2");
        thread2.start();

        // 啟動第三個售票視窗
        Thread thread3 = new Thread(ticket, "售票視窗3");
        thread3.start();
    }
}

class TicketRunnable implements Runnable {
    // 總票數,不必定義為 static,因為只需要 new 一次 TicketRunnable
    private int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
            } else {
                break;
            }
        }
    }
}

說明:

  1. 如上程式,在買票的過程中,出現了重票、錯票,說明多執行緒的執行過程中,出現了安全問題。

  2. 問題的原因:當多條語句在操作同一個執行緒的共享資料時,當一個執行緒對多條語句只執行了一部分,還沒有執行完時,另一個執行緒參與進來執行,從而導致了共享資料的錯誤。

  3. 解決辦法:對多條操作共享資料的語句,讓一個執行緒全部執行完,在執行的過程中,其他執行緒不可以參與執行。

執行緒的同步機制

對於多執行緒的安全問題,Java 提供了專業的解決方式:同步機制。實現同步機制的方式,有同步程式碼塊同步方法Lock 鎖等多種形式。

同步的範圍

  1. 如何找問題,即程式碼是否存線上程安全?--- 非常重要
    (1)明確哪些程式碼是多執行緒執行的程式碼。
    (2)明確多個執行緒是否有共享資料。
    (3)明確多執行緒執行程式碼中是否有多條語句操作共享資料。

  2. 如何解決呢?--- 非常重要

    對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行,即所有操作共享資料的這些語句都要放在同步範圍中。

  3. 切記 :

    範圍太小:沒鎖住所有有安全問題的程式碼。

    範圍太大:沒發揮多執行緒的功能。

同步機制的特點

優點:同步的方式,能夠解決執行緒的安全問題。

侷限性:操作同步程式碼時,只能有一個執行緒參與,其他執行緒等待,相當於是一個單執行緒的過程,效率低。

共享資料:多個執行緒共同操作的變數。

需要被同步的程式碼:操作共享資料的程式碼。

同步監視器,俗稱:鎖。任何一個類的物件,都可以充當鎖。

要求:多個執行緒必須要公用同一把鎖!!!針對不同實現同步機制的方式,都要保證同步監視器是同一個!!!

同步機制中的鎖

同步鎖機制:在《Thinking in Java》中,是這麼說的:對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共享資源競爭)。防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。

synchronized 的鎖是什麼:

  • 任意物件都可以作為同步鎖,所有物件都自動含有單一的鎖(監視器)。

  • 同步程式碼塊的鎖:自己指定,很多時候是指定為this類名.class

  • 同步方法的鎖:靜態方法 ---> 類名.class,非靜態方法 ---> this

  • 注意:

    • 必須確保使用同一個資源的多個執行緒共用的是同一把鎖,這個非常重要,否則就無法保證共享資源的安全。
    • 一個執行緒類中的所有靜態方法共用同一把鎖 --- 類名.class,所有非靜態方法共用同一把鎖 --- this,同步程式碼塊在指定鎖的時候需謹慎。

能夠釋放鎖的操作:

  • 當前執行緒的同步方法、同步程式碼塊執行結束。
  • 當前執行緒在同步程式碼塊、同步方法中遇到 break、return 終止了該程式碼塊、該方法的繼續執行。
  • 當前執行緒在同步程式碼塊、同步方法中出現了未處理的 Error 或 Exception,導致異常結束。
  • 當前執行緒在同步程式碼塊、同步方法中執行了執行緒物件的wait(),當前執行緒暫停,並釋放鎖。

不會釋放鎖的操作:

  • 執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()Thread.yield()暫停當前執行緒的執行。
  • 執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()將該執行緒掛起,該執行緒不會釋放鎖。
  • 應儘量避免使用suspend()resume()來控制執行緒。

同步機制一:同步程式碼塊

格式:

synchronized (同步監視器) {
    // 需要被同步的程式碼
}

繼承 Thread 類方式的修正:

public class TestThread {
    public static void main(String[] args) {
        // 啟動第一個售票視窗
        TicketThread thread1 = new TicketThread();
        thread1.setName("售票視窗一");
        thread1.start();

        // 啟動第二個售票視窗
        TicketThread thread2 = new TicketThread();
        thread2.setName("售票視窗二");
        thread2.start();

        // 啟動第三個售票視窗
        TicketThread thread3 = new TicketThread();
        thread3.setName("售票視窗三");
        thread3.start();

    }
}

class TicketThread extends Thread {
    // 總票數,必須定義為 static,隨類只載入一次,因為每新建一個執行緒,都需要 new 一次 TicketThread
    private static int ticketNum = 100;

    // 鎖,必須定義為 static
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {// 可以使用:synchronized (TicketThread.class),不能使用:synchronized (this)
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
                } else {
                	break;
            	}
            }
        }
    }
}

obj 可以使用 TicketThread.class(當前類)替代,TicketThread 類只會載入一次,類也是物件。

實現 Runnable 介面方式的修正:

public class TestRunnable {
    public static void main(String[] args) {
        TicketRunnable ticket = new TicketRunnable();

        // 啟動第一個售票視窗
        Thread thread1 = new Thread(ticket, "售票視窗1");
        thread1.start();

        // 啟動第二個售票視窗
        Thread thread2 = new Thread(ticket, "售票視窗2");
        thread2.start();

        // 啟動第三個售票視窗
        Thread thread3 = new Thread(ticket, "售票視窗3");
        thread3.start();
    }
}


class TicketRunnable implements Runnable {
    // 總票數,不必定義為 static,因為只需要 new 一次 TicketRunnable
    private int ticketNum = 100;

    // 鎖,不必定義為 static
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {// 可以使用:synchronized (this)
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
                } else {
                    break;
                }
            }
        }
    }
}

obj 物件可以使用 this 代替,指代唯一的 TicketRunnable 物件。

同步機制二:同步方法

格式:

修飾符 synchronized 返回值型別 方法名 (形參列表) {}

如果操作共享資料的程式碼,完整的宣告在一個方法中,則可以將此方法宣告為同步方法。

同步方法仍然涉及到同步監視器,只是不需要顯示的宣告:

  • 非靜態的同步方法,同步監視器是:this。
  • 靜態的同步方法,同步監視器是:當前類本身。

繼承 Thread 類方式的修正:

public class TestMethod1 {
    public static void main(String[] args) {
        // 啟動第一個售票視窗
        TicketMethod1 thread1 = new TicketMethod1();
        thread1.setName("售票視窗一");
        thread1.start();

        // 啟動第二個售票視窗
        TicketMethod1 thread2 = new TicketMethod1();
        thread2.setName("售票視窗二");
        thread2.start();

        // 啟動第三個售票視窗
        TicketMethod1 thread3 = new TicketMethod1();
        thread3.setName("售票視窗三");
        thread3.start();
    }
}

class TicketMethod1 extends Thread {
    // 總票數,必須定義為 static,隨類只載入一次,因為每新建一個執行緒,都需要 new 一次 TicketThread
    private static int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            handleTicket();
        }
    }

    // 必須設定成 static 的,此時的同步監視器是 TicketMethod1.class
    private static synchronized void handleTicket() {
        if (ticketNum > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
        } else {
            break;
        }
    }
}

此時,同步方法要設定成 static 的,此時的同步監視器是 TicketMethod1.class(當前類)。

實現 Runnable 介面方式的修正:

public class TestMethod2 {
    public static void main(String[] args) {
        TicketMethod2 ticket = new TicketMethod2();

        // 啟動第一個售票視窗
        Thread thread1 = new Thread(ticket, "售票視窗1");
        thread1.start();

        // 啟動第二個售票視窗
        Thread thread2 = new Thread(ticket, "售票視窗2");
        thread2.start();

        // 啟動第三個售票視窗
        Thread thread3 = new Thread(ticket, "售票視窗3");
        thread3.start();
    }
}

class TicketMethod2 implements Runnable {
    private int ticketNum = 100;

    @Override
    public void run() {// 有時可以直接設定 run 方法為 synchronized,但本例不行
        while (true) {
            handleTicket();
        }
    }

    // 非靜態同步方法中,同步監視器:this
    private synchronized void handleTicket() {
        if (ticketNum > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
        } else {
            break;
        }
    }
}

此時,同步方法中的同步監視器是:this,即當前 TicketMethod2 類的物件。

同步機制三:Lock 鎖

從 JDK 5.0 開始,Java 提供了更強大的執行緒同步機制——透過顯式定義同步鎖物件來實現同步。同步鎖使用 Lock 物件充當。

  • java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對 Lock 物件加鎖,執行緒開始訪問共享資源之前應先獲得 Lock 物件。

  • 在實現執行緒安全的控制中,比較常用的是ReentrantLock,ReentrantLock 類實現了 Lock 介面,它擁有與 synchronized 相同的併發性和記憶體語義,可以顯式加鎖、釋放鎖。

宣告格式:

image-20210310103811885

繼承 Thread 類方式的修正:

public class LockTest {
    public static void main(String[] args) {
        // 啟動第一個售票視窗
        Ticket thread1 = new Ticket();
        thread1.setName("售票視窗一");
        thread1.start();

        // 啟動第二個售票視窗
        Ticket thread2 = new Ticket();
        thread2.setName("售票視窗二");
        thread2.start();

        // 啟動第三個售票視窗
        Ticket thread3 = new Ticket();
        thread3.setName("售票視窗三");
        thread3.start();
    }
}

class Ticket extends Thread {
    private static int ticketNum = 100;

    // 1. 例項化靜態 ReentrantLock
    private static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 2. 呼叫鎖定方法:lock()
            lock.lock();
            try {
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();// 3. 呼叫解鎖方法:unlock()
            }
        }
    }
}

ReentrantLock 例項物件需要設定為 static。

實現 Runnable 介面方式的修正:

public class LockTest {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        // 啟動第一個售票視窗
        Thread thread1 = new Thread(ticket, "售票視窗1");
        thread1.start();

        // 啟動第二個售票視窗
        Thread thread2 = new Thread(ticket, "售票視窗2");
        thread2.start();

        // 啟動第三個售票視窗
        Thread thread3 = new Thread(ticket, "售票視窗3");
        thread3.start();
    }
}

class Ticket implements Runnable {
    private int ticketNum = 100;

    // 1. 例項化ReentrantLock
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 2. 呼叫鎖定方法:lock()
            lock.lock();
            try {
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + ticketNum--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();// 3. 呼叫解鎖方法:unlock()
            }
        }
    }
}

synchronized 和 Lock 的對比

  • synchronized 是隱式鎖,出了作用域自動釋放同步監視器,而 Lock 是顯式鎖,需要手動開啟和關閉鎖。
  • synchronized 有程式碼塊鎖和方法鎖,而 Lock 只有程式碼塊鎖。
  • 使用 Lock 鎖,JVM 將花費較少的時間來排程執行緒,效能更好,並且具有更好的擴充套件性(Lock 介面能提供更多的實現類)。

優先使用順序:Lock ---> 同步程式碼塊(已經進入了方法體,分配了相應資源)---> 同步方法(在方法體之外)

經典例項

銀行有一個賬戶,有兩個儲戶分別向這個賬戶存錢,每次存 1000,存 10 次,要求每次存完列印賬戶餘額。

實現方式一:

public class AccountTest {
    public static void main(String[] args) {
        // 一個賬戶
        Account account = new Account(0.0);

        // 兩個儲戶
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public double getBalance() {
        return balance;
    }

    // 此時的鎖是 Accout 的物件,本例的寫法中,Account 只有一個,所以兩個執行緒公用的是一個同步鎖
    public synchronized void deposit(double amt) {
        if (amt > 0) {
            balance += amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存錢成功,餘額為:" + balance);
        }
    }
}

class Customer extends Thread {
    private Account account;

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            account.deposit(1000.0);
        }
    }
}

實現方式二:

public class AccountTest {
    public static void main(String[] args) {
        // 一個賬戶
        Account account = new Account(0.0);

        // 兩個儲戶
        Customer c1 = new Customer(account);
        Customer c2 = new Customer(account);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amt) {
        if (amt > 0) {
            balance += amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存錢成功,餘額為:" + balance);
        }
    }
}

class Customer extends Thread {
    private Account account;
    
    // static 的 Lock
    private static Lock lock = new ReentrantLock();

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                account.deposit(1000.0);
            } finally {
                lock.unlock();
            }
        }
    }
}

執行緒的通訊

  • wait()notify()notifyAll()

    • wait()一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器。
      • 當前執行緒排隊等候其他執行緒呼叫notify()notifyAll()方法喚醒,喚醒後等待重新獲得對監視器的所有權後才能繼續執行。
      • 被喚醒的執行緒從斷點處繼續程式碼的執行。
    • notify():一旦執行此方法,就會喚醒被wait()的一個執行緒。如果有多個執行緒被wait(),則喚醒優先順序高的。
    • notifyAll():一旦執行此方法,就會喚醒所有被wait()的執行緒。
  • wait()notify()notifyAll()這三個方法必須使用在同步程式碼塊或同步方法中。

  • wait()notify()notifyAll()這三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。

    • 否則會出現java.lang.IllegalMonitorStateException異常。
  • wait()notify()notifyAll()這三個方法是定義在java.lang.Object類中的。

    • 因為這三個方法必須由同步監視器呼叫,而任意物件都可以作為同步監視器,因此這三個方法只能在 Object 類中宣告。

示例一:使用兩個執行緒列印 1 - 100,要求執行緒 1 和執行緒 2 交替列印。

public class CommunicationTest {
  public static void main(String[] args) {
      Number number = new Number();

      Thread t1 = new Thread(number);
      Thread t2 = new Thread(number);

      t1.setName("執行緒1");
      t2.setName("執行緒2");

      t1.start();
      t2.start();
  }
}

class Number implements Runnable {
  private int number = 1;

  @Override
  public void run() {
      while (true) {
          synchronized (this) {
              // 喚醒被 wait() 的一個執行緒
              notify();// 等同於:this.notify();
              if (number <= 100) {
                  try {
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }

                  System.out.println(Thread.currentThread().getName() + ": " + number);
                  number++;

                  try {
                      // 使呼叫 wait() 方法的執行緒進入阻塞狀態
                      wait();// 等同於:this.wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              } else {
                  break;
              }
          }
      }
  }
}

示例二:生產者/消費者問題。

生產者(Producer)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如 20),如果生產者試圖生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer producer1 = new Producer(clerk);
        producer1.setName("生產者1");

        Consumer consumer1 = new Consumer(clerk);
        consumer1.setName("消費者1");
        Consumer consumer2 = new Consumer(clerk);
        consumer2.setName("消費者2");

        producer1.start();
        consumer1.start();
        consumer2.start();
    }
}

class Clerk {
    private int productCount = 0;

    public synchronized void produceProduct() {
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + "開始生產第" + productCount + "個產品");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consumerProduct() {
        if (productCount > 0) {
            System.out.println(Thread.currentThread().getName() + "開始消費第" + productCount + "個產品");
            productCount--;
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 生產者
class Producer extends Thread {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始生產產品...");
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}

// 消費者
class Consumer extends Thread {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始消費產品...");
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumerProduct();
        }
    }
}

面試題:sleep()wait()的異同。

  • 相同點:一旦執行方法,都可以使得當前的執行緒進入阻塞狀態。
  • 不同點:
    • 兩個方法宣告的位置不同:`sleep()`宣告在 Thread 類中,`wait()`宣告在 Object 類中。
      
    • 呼叫的要求不同:`sleep()`可以在任何需要的場景下呼叫,`wait()`必須使用在同步程式碼塊或同步方法中。
      
    • 關於是否釋放同步監視器:如果兩個方法都是用在同步程式碼塊或同步方法中,`sleep()`不會釋放鎖,`wait()`會釋放鎖。
      

執行緒的死鎖問題

死鎖

  • 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖。

  • 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續。

示例一:

public class DeadLock {
  public static void main(String[] args) {
      StringBuilder s1 = new StringBuilder();
      StringBuilder s2 = new StringBuilder();

      // 繼承 Thread 類
      new Thread() {
          @Override
          public void run() {
              synchronized (s1) {
                  s1.append("a");
                  s2.append(1);

                  // 新增 sleep(),增加死鎖觸發的機率
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }

                  synchronized (s2) {
                      s1.append("b");
                      s2.append(2);

                      System.out.println(s1);
                      System.out.println(s2);
                  }
              }
          }
      }.start();

      // 實現 Runnable 介面
      new Thread(new Runnable() {
          @Override
          public void run() {
              synchronized (s2) {
                  s1.append("c");
                  s2.append(3);

                  // 新增 sleep(),增加死鎖觸發的機率
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }

                  synchronized (s1) {
                      s1.append("d");
                      s2.append(4);

                      System.out.println(s1);
                      System.out.println(s2);
                  }
              }
          }
      }).start();
  }
}

示例二:

class A {
    public synchronized void foo(B b) {// 同步監視器:A 的物件
        System.out.println("當前執行緒名: " + Thread.currentThread().getName() + ", 進入了A例項的foo方法"); // ①
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("當前執行緒名: " + Thread.currentThread().getName() + ", 企圖呼叫B例項的last方法"); // ③
        b.last();
    }

    public synchronized void last() {
        System.out.println("進入了A類的last方法內部");
    }
}

class B {
    public synchronized void bar(A a) {// 同步監視器:B 的物件
        System.out.println("當前執行緒名: " + Thread.currentThread().getName() + ", 進入了B例項的bar方法"); // ②
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("當前執行緒名: " + Thread.currentThread().getName() + ", 企圖呼叫A例項的last方法"); // ④
        a.last();
    }

    public synchronized void last() {
        System.out.println("進入了B類的last方法內部");
    }
}

public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主執行緒");
        // 呼叫 a 物件的 foo 方法
        a.foo(b);
        System.out.println("進入了主執行緒之後");
    }

    @Override
    public void run() {
        Thread.currentThread().setName("副執行緒");
        // 呼叫 b 物件的 bar 方法
        b.bar(a);
        System.out.println("進入了副執行緒之後");
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        new Thread(deadLock).start();
        deadLock.init();
    }
}

解決死鎖的方法:

  • 專門的演算法、原則。
  • 儘量減少同步資源的定義。
  • 儘量避免巢狀同步。

執行緒池

執行緒池是預先建立執行緒的一種技術,執行緒池在還沒有任務到來之前,事先建立一定數量的執行緒,放入空閒佇列中,然後對這些資源進行復用,從而減少頻繁的建立和銷燬物件。

系統啟動一個新執行緒的成本是比較高的,因為它涉及與作業系統互動。在這種情形下,使用執行緒池可以很好地提高效能,尤其是當程式中需要建立大量生存期很短暫的執行緒時,更應該考慮使用執行緒池。

與資料庫連線池類似的是,執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個 Runnable 物件或 Callable 物件傳給執行緒池,執行緒池就會啟動一個執行緒來執行它們的run()call(), 當run()call()執行結束後, 該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等待執行下一個 Runnable 物件或 Callable 物件的run()call()

總結:由於系統建立和銷燬執行緒都是需要時間和系統資源開銷,為了提高效能,才考慮使用執行緒池。執行緒池會在系統啟動時就建立大量的空閒執行緒,然後等待新的執行緒呼叫,執行緒執行結束並不會銷燬,而是重新進入執行緒池,等待再次被呼叫。這樣子就可以減少系統建立啟動和銷燬執行緒的時間,提高系統的效能。

使用 Executors 建立執行緒池

Executor 是執行緒池的頂級介面,介面中只定義了一個方法void execute(Runnable command);,執行緒池的操作方法都是定義在 ExecutorService 子介面中的,所以說ExecutorService 是執行緒池真正的介面

newSingleThreadExecutor

建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newFixedThreadPool

建立固定大小的執行緒池,每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到是大值就會保持不變。如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

newCachedThreadPool

建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60 秒不執行任務)的執行緒。當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對錢程池大小做限制,執行緒池大小完全依賴於作業系統(或者說 JVM)能夠建立的最大執行緒大小。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

使用 ThreadPoolExecutor 建立執行緒池

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

建構函式引數說明

corePoolSize:核心執行緒數大小,當執行緒數小於 corePoolSize 的時候,會建立執行緒執行新的 runnable 或 callable。

maximumPoolSize:最大執行緒數, 當執行緒數大於等於 corePoolSize 的時候,會把新的 runnable 或 callable 放入 workQueue 中。

keepAliveTime:保持存活時間,當執行緒數大於 corePoolSize 的時候,空閒執行緒能保持的最大時間。

unit:時間單位。

workQueue:儲存任務的阻塞佇列

threadFactory:建立執行緒的工廠。

handler:拒絕策略

任務執行順序

  • 當執行緒數小於 corePoolSize 時,建立執行緒執行新任務。

  • 當執行緒數大於等於 corePoolSize,並且 workQueue 沒有滿時,新任務放入 workQueue 中。

  • 當執行緒數大於等於 corePoolSize,並且 workQueue 滿時,新任務建立新執行緒執行,但執行緒總數要小於 maximumPoolSize。

  • 當執行緒總數等於 maximumPoolSize,並且 workQueue 滿時,執行 handler 的 rejectedExecution,也就是拒絕策略。

1698120774628

阻塞佇列

阻塞佇列是一個在佇列基礎上又支援了兩個附加操作的佇列:

  1. 支援阻塞的插入方法:佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
  2. 支援阻塞的移除方法:佇列空時,獲取元素的執行緒會等待佇列變為非空。
阻塞佇列的應用場景

阻塞佇列常用於生產者和消費者的場景,生產者是向佇列裡新增元素的執行緒,消費者是從佇列裡取元素的執行緒。簡而言之,阻塞佇列是生產者用來存放元素、消費者獲取元素的容器。

阻塞佇列的方法

在阻塞佇列不可用的時候,上述兩個附加操作提供了四種處理方法:

方法處理方式 丟擲異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
檢查方法 element() peek() 不可用 不可用
阻塞佇列的型別

JDK 7 提供了 7 個阻塞佇列,如下:

  1. ArrayBlockingQueue:陣列結構組成的有界阻塞佇列。

    • 此佇列按照先進先出(FIFO)的原則對元素進行排序,但是預設情況下不保證執行緒公平的訪問佇列,即如果佇列滿了,那麼被阻塞在外面的執行緒對佇列訪問的順序是不能保證執行緒公平(即先阻塞,先插入)的。
  2. LinkedBlockingQueue:一個由連結串列結構組成的有界阻塞佇列。

    • 此佇列按照先出先進的原則對元素進行排序。
  3. PriorityBlockingQueue:支援優先順序的無界阻塞佇列。

  4. DelayQueue:支援延時獲取元素的無界阻塞佇列,即可以指定多久才能從佇列中獲取當前元素。

  5. SynchronousQueue:不儲存元素的阻塞佇列,每一個 put 必須等待一個 take 操作,否則不能繼續新增元素。並且支援公平訪問佇列。

  6. LinkedTransferQueue:由連結串列結構組成的無界阻塞 TransferQueue 佇列。

    • 相對於其他阻塞佇列,多了 transfer 和 tryTransfer 方法:
      • transfer 方法:如果當前有消費者正在等待接收元素(take 或者待時間限制的 poll 方法),transfer 可以把生產者傳入的元素立刻傳給消費者。如果沒有消費者等待接收元素,則將元素放在佇列的 tail 節點,並等到該元素被消費者消費了才返回。
      • tryTransfer 方法:用來試探生產者傳入的元素能否直接傳給消費者。如果沒有消費者在等待,則返回 false。和上述方法的區別是該方法無論消費者是否接收,方法立即返回,而 transfer 方法是必須等到消費者消費了才返回。
  7. LinkedBlockingDeque:連結串列結構的雙向阻塞佇列,優勢在於多執行緒入隊時,減少一半的競爭。

拒絕策略

當佇列和執行緒池都滿了,說明執行緒池處於飽和的狀態,那麼必須採取一種策略處理提交的新任務。ThreadPoolExecutor 預設有四個拒絕策略:

  • ThreadPoolExecutor.AbortPolicy():預設策略,直接丟擲異常 RejectedExecutionException。

    java.util.concurrent.RejectedExecutionException:

    當執行緒池 ThreadPoolExecutor 執行方法shutdown()之後,再向執行緒池提交任務的時候,如果配置的拒絕策略是 AbortPolicy ,這個異常就會丟擲來。

    當設定的任務快取佇列過小的時候,或者說,執行緒池裡面所有的執行緒都在幹活(執行緒數等於 maxPoolSize),並且任務快取佇列也已經充滿了等待的佇列, 這個時候,再向它提交任務,也會丟擲這個異常。

  • ThreadPoolExecutor.CallerRunsPolicy():直接使用當前執行緒(一般是 main 執行緒)呼叫run()方法並且阻塞執行。

  • ThreadPoolExecutor.DiscardPolicy():不處理,直接丟棄後來的任務。

  • ThreadPoolExecutor.DiscardOldestPolicy():丟棄在佇列中隊首的任務,並執行當前任務。

當然可以繼承 RejectedExecutionHandler 來自定義拒絕策略。

執行緒池引數選擇

CPU 密集型:執行緒池的大小推薦為 CPU 數量 +1。CPU 數量可以根據Runtime.getRuntime().availableProcessors()方法獲取。

I/O 密集型:CPU 數量 * CPU 利用率 *(1 + 執行緒等待時間 / 執行緒 CPU 時間)。

混合型:將任務分為 CPU 密集型和 I/O 密集型,然後分別使用不同的執行緒池去處理,從而使每個執行緒池可以根據各自的工作負載來調整。

阻塞佇列:推薦使用有界佇列,有界佇列有助於避免資源耗盡的情況發生。

拒絕策略:預設採用的是 AbortPolicy 拒絕策略,直接在程式中丟擲 RejectedExecutionException 異常,因為是執行時異常,不強制 catch,但這種處理方式不夠優雅。處理拒絕策略有以下幾種比較推薦:

  • 在程式中捕獲 RejectedExecutionException 異常,在捕獲異常中對任務進行處理。針對預設拒絕策略。
  • 使用 CallerRunsPolicy 拒絕策略,該策略會將任務交給呼叫 execute 的執行緒執行(一般為主執行緒),此時主執行緒將在一段時間內不能提交任何任務,從而使工作執行緒處理正在執行的任務。此時提交的執行緒將被儲存在 TCP 佇列中,TCP 佇列滿將會影響客戶端,這是一種平緩的效能降低。
  • 自定義拒絕策略,只需要實現 RejectedExecutionHandler 介面即可。
  • 如果任務不是特別重要,使用 DiscardPolicy 和 DiscardOldestPolicy 拒絕策略將任務丟棄也是可以的。

如果使用 Executors 的靜態方法建立 ThreadPoolExecutor 物件,可以透過使用 Semaphore 對任務的執行進行限流也可以避免出現 OOM 異常。

執行緒池關閉

等待所有執行緒執行完畢後,應關閉執行緒池:

try {
    // 等待所有執行緒執行完畢當前任務
    threadPool.shutdown();

    boolean loop = true;
    do {
        // 等待所有執行緒執行完畢,當前任務結束
        loop = !threadPool.awaitTermination(2, TimeUnit.SECONDS);// 等待 2 秒
    } while (loop);

    if (!loop) {
        System.out.println("所有執行緒執行完畢");
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    System.out.println("耗時:" + (System.currentTimeMillis() - startTimeMillis));
}

如果只需要等待模型特定任務完成,可以參考如下方式:

Map<String, Future<?>> jobFutureMap = new HashMap<String, Future<?>>();
for (String key : noneExsitKeys) {
    ConcurrentAccessDBJob job = new ConcurrentAccessDBJob(key, userLevel, dao, service);
    Future<?> future = threadPool.submit(job);
    jobFutureMap.put(key, future);
}
for (String key : noneExsitKeys) {
    Future<?> future = jobFutureMap.get(key);
    // 呼叫此方法會使主執行緒等待子執行緒完成
    future.get();
    System.out.println("future.idDone()" + future.isDone());
}

示例

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
    public static void main(String[] args) {
        long startTimeMillis = System.currentTimeMillis();

        // 構造一個執行緒池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3));

        for (int i = 1; i <= 10; i++) {
            try {
                String task = "task = " + i;
                System.out.println("建立任務並提交到執行緒池中:" + task);
                threadPool.execute(new ThreadPoolTask(task));
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 執行緒池關閉
        try {
            // 等待所有執行緒執行完畢當前任務
            threadPool.shutdown();

            boolean loop = true;
            do {
                // 等待所有執行緒執行完畢,當前任務結束
                loop = !threadPool.awaitTermination(2, TimeUnit.SECONDS);// 等待 2 秒
            } while (loop);

            if (!loop) {
                System.out.println("所有執行緒執行完畢");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("耗時:" + (System.currentTimeMillis() - startTimeMillis));
        }
    }
}
import java.io.Serializable;

public class ThreadPoolTask implements Runnable, Serializable {
    private String attachData;

    public ThreadPoolTask(String tasks) {
        this.attachData = tasks;
    }

    public void run() {
        try {
            System.out.println("開始執行:" + attachData + "任務,使用的執行緒池,執行緒名稱:"
                    + Thread.currentThread().getName() + "\r\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
        attachData = null;
    }
}

執行結果,可以看到執行緒 pool-1-thread-1 到 pool-1-thread-5 迴圈使用:

建立任務並提交到執行緒池中:task = 1
開始執行:task = 1任務,使用的執行緒池,執行緒名稱:pool-1-thread-1

建立任務並提交到執行緒池中:task = 2
開始執行:task = 2任務,使用的執行緒池,執行緒名稱:pool-1-thread-2

建立任務並提交到執行緒池中:task = 3
開始執行:task = 3任務,使用的執行緒池,執行緒名稱:pool-1-thread-3

建立任務並提交到執行緒池中:task = 4
開始執行:task = 4任務,使用的執行緒池,執行緒名稱:pool-1-thread-4

建立任務並提交到執行緒池中:task = 5
開始執行:task = 5任務,使用的執行緒池,執行緒名稱:pool-1-thread-5

建立任務並提交到執行緒池中:task = 6
開始執行:task = 6任務,使用的執行緒池,執行緒名稱:pool-1-thread-1

建立任務並提交到執行緒池中:task = 7
開始執行:task = 7任務,使用的執行緒池,執行緒名稱:pool-1-thread-2

建立任務並提交到執行緒池中:task = 8
開始執行:task = 8任務,使用的執行緒池,執行緒名稱:pool-1-thread-3

建立任務並提交到執行緒池中:task = 9
開始執行:task = 9任務,使用的執行緒池,執行緒名稱:pool-1-thread-4

建立任務並提交到執行緒池中:task = 10
開始執行:task = 10任務,使用的執行緒池,執行緒名稱:pool-1-thread-5

所有執行緒執行完畢
耗時:1014

優雅實現

package cn.antai.xisun.influxdb.utils;

import cn.hutool.core.thread.ThreadUtil;

import java.util.concurrent.*;

/**
 * 可伸縮的執行緒池,可根據當前任務數自動調整 corePoolSize
 * 實際場景中,有時很難估算出合理的執行緒數
 * 參考美團技術團隊部落格而做此實現,詳情見:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
 */
public class FlexibleThreadPool {
    private static final int CORE_POOL_SIZE = 20;
    private static final int MAX_POOL_SIZE = 50;
    private static final long KEEP_ALIVE_MINUTES = 3L;
    private static final int MAX_TASK_SIZE = 1000;
    private static final double THREAD_INCREASE_FACTOR = 1.5;

    // 可動態調整 coreSize 大小的執行緒池
    private static final ThreadPoolExecutor THREAD_POOL = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAX_POOL_SIZE,
            KEEP_ALIVE_MINUTES,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(MAX_TASK_SIZE),
            ThreadUtil.newNamedThreadFactory("flexible-thread-pool-", false),
            new ThreadPoolExecutor.CallerRunsPolicy());

    static {
        THREAD_POOL.allowCoreThreadTimeOut(true);

        // 開啟定期更新 threadPool 的 coreSize 任務
        schedule();
    }

    private static void schedule() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "AdjustFlexibleThreadPoolCoreSize");
            thread.setDaemon(true);
            return thread;
        });
        // 每 3 秒檢測是否需要調整 corePoolSize
        final long period = 3L;
        scheduler.scheduleAtFixedRate(FlexibleThreadPool::adjustThreadPoolCoreSize, 1, period, TimeUnit.SECONDS);
    }

    private static void adjustThreadPoolCoreSize() {
        final int minThreads = CORE_POOL_SIZE;
        final int maxThreads = MAX_POOL_SIZE;
        final double factor = THREAD_INCREASE_FACTOR;

        final int coreSize = THREAD_POOL.getCorePoolSize();
        int size = coreSize;
        final int n = (int) (coreSize / factor);

        if (!THREAD_POOL.getQueue().isEmpty()) {
            // 任務佇列中有排隊任務,應適當增加 coreSize
            size = (int) (coreSize * factor);
        } else if (THREAD_POOL.getActiveCount() <= n) {
            // 執行緒池中活躍執行緒數低於 coreSize/factor 時,應適當減少 coreSize
            size = n + 3;
        } else {
            return;
        }
        // coreSize 不能低於 minThreads,也不能高於 maxThreads
        size = Math.min(Math.max(minThreads, size), maxThreads);

        // 這個判斷是必須的(coreSize 不需要改變時不要呼叫 setCorePoolSize,否則會頻繁 interrupt 因 poll 而阻塞的 thread)
        if (size != coreSize) {
            THREAD_POOL.setCorePoolSize(size);
        }
    }

    public static ThreadPoolExecutor getThreadPool() {
        return THREAD_POOL;
    }

    public static <T> Future<T> submitTask(Callable<T> task) {
        return THREAD_POOL.submit(task);
    }

    public static void submitTask(Runnable task) { 
        THREAD_POOL.submit(task);
    }
}

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/JavaLanguage/java-advanced.md

相關文章