Java併發編程系列之二線程基礎

来源:https://www.cnblogs.com/tianClassmate/archive/2022/04/07/16112688.html
-Advertisement-
Play Games

上篇文章對併發的理論基礎進行了回顧,主要是為什麼使用多線程、多線程會引發什麼問題及引發的原因,和怎麼使用Java中的多線程去解決這些問題。 正所謂,知其然知其所以然,這是學習一個知識遵循的原則。 推薦讀者先行查看併發編程的理論知識,以便可以絲滑入戲。 併發編程系列之一併發理論基礎 本篇文章重點在於J ...


上篇文章對併發的理論基礎進行了回顧,主要是為什麼使用多線程、多線程會引發什麼問題及引發的原因,和怎麼使用Java中的多線程去解決這些問題。

正所謂,知其然知其所以然,這是學習一個知識遵循的原則。

推薦讀者先行查看併發編程的理論知識,以便可以絲滑入戲。

併發編程系列之一併發理論基礎

本篇文章重點在於Java中怎麼去使用多線程,和多線程的一些相關概念和操作,及怎麼優化多線程。

在Java中每個對象都有其生命周期,線程同樣不例外,也有其生命周期。

一、線程生命周期

線程的幾種狀態轉換

image-20220407104430832

1、新建(New)

新創建了一個線程對象,但還沒有調用start()方法。

2、就緒

當線程對象調用了start()方法後,該線程就進入就緒狀態。處於就緒狀態的線程位於線程隊列中,此時它只是具備了運行的條件,能否獲得CPU的使用權並開始運行,還需要等待系統的調度。

3、運行(Runnable)

如果處於就緒狀態的線程獲得了CPU的使用權,並開始執行run()方法中的線程執行體,則該線程處於運行狀態。

一個線程啟動後,它可能不會一直處於運行狀態,當運行狀態的線程使用完系統分配的時間後,系統就會剝奪該線程占用的CPU資源,讓其他線程獲得執行的機會。需要註意的是,

只有處於就緒狀態的線程才可能轉換到運行狀態。

4、阻塞(Blocking)

等待獲取一個排它鎖,如果其線程釋放了鎖就會結束此狀態。

①無限期等待(Waiting)

等待其它線程顯式地喚醒,否則不會被分配 CPU 時間片。

進入方法 退出方法
沒有設置 Timeout 參數的 Object.wait() 方法 Object.notify() / Object.notifyAll()
沒有設置 Timeout 參數的 Thread.join() 方法 被調用的線程執行完畢
LockSupport.park() 方法 -

②限期等待(Timed Waiting)

無需等待其它線程顯式地喚醒,在一定時間之後會被系統自動喚醒。

調用 Thread.sleep() 方法使線程進入限期等待狀態時,常常用“使一個線程睡眠”進行描述。

調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,常常用“掛起一個線程”進行描述。

睡眠和掛起是用來描述行為,而阻塞和等待用來描述狀態。

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,通過調用 Thread.sleep() 和 Object.wait() 等方法進入。

進入方法 退出方法
Thread.sleep() 方法 時間結束
設置了 Timeout 參數的 Object.wait() 方法 時間結束 / Object.notify() / Object.notifyAll()
設置了 Timeout 參數的 Thread.join() 方法 時間結束 / 被調用的線程執行完畢
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -

5、死亡(Terminated)

如果線程調用stop()方法或nun()方法正常執行完畢,或者線程拋出一個未捕獲的異常(Exception)錯誤(Error),線程就進入死亡狀態。一旦進入死亡狀態,線程將不再擁有運行的資格,也不能再轉換到其他狀態。

理解線程的五種狀態,在調用多線程的方法時,能清楚的知道當前處於哪個狀態。

我們舉一個簡單的實例來說明每個狀態。

public class MyThread extends Thread {
    
    //運行狀態
    public void run() {
        // ...
    }
    
    public static void main(String[] args) {
    MyThread mt = new MyThread(); //1、新建狀態
    mt.start(); //就緒狀態
}
}
     

線上程式控制制章節有一些方法,如sleep()\join()方法,這些方法會讓線程處於阻塞狀態。

瞭解了線程的生成周期以後,接下來我們就需要掌握在Java中怎麼使用多線程。

在Java中有三種方式實現多線程。

二、創建線程的三種方式

有三種使用線程的方法:

  • 實現 Runnable 介面;
  • 實現 Callable 介面;
  • 繼承 Thread 類。

實現 Runnable 和 Callable 介面的類只能當做一個可以線上程中運行的任務,不是真正意義上的線程,因此最後還需要通過 Thread 來調用。可以說任務是通過線程驅動從而執行的。

1、實現 Runnable 介面

需要實現 run() 方法。

通過 Thread 調用 start() 方法來啟動線程。

public class MyRunnable implements Runnable {
    public void run() {
        // 需要執行多線程的業務邏輯
    }
}
    
public static void main(String[] args) {
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
}
     

2、 實現 Callable 介面

與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
  

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
  

3、繼承 Thread 類

同樣也是需要實現 run() 方法,因為 Thread 類也實現了 Runable 介面。

當調用 start() 方法啟動一個線程時,虛擬機會將該線程放入就緒隊列中等待被調度,當一個線程被調度時會執行該線程的 run() 方法。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
  
         
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
  
       

4、實現介面 VS 繼承 Thread

實現介面會更好一些,因為:

  • Java 不支持多重繼承,因此繼承了 Thread 類就無法繼承其它類,但是可以實現多個介面;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。

三、線程式控制制

線程在使用過程中能對其靈活的控制,包含線程睡眠和線程讓步等。

在學習線程的一些控制方法前,有一個必須要瞭解的前置知識,線上程中分為守護進程和非守護進程。

1、Daemon

守護線程是程式運行時在後臺提供服務的線程,不屬於程式中不可或缺的部分。

當所有非守護線程結束時,程式也就終止,同時會殺死所有守護線程。

垃圾回收線程就是一個經典的守護線程,當我們的程式中不再有任何運行的Thread,程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。

main() 屬於非守護線程。

非守護線程可以轉換為守護進程。

使用 setDaemon() 方法將一個線程設置為守護線程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
  

2、sleep()

Thread.sleep(millisec) 方法會休眠當前正在執行的線程,millisec 單位為毫秒。

sleep() 可能會拋出 InterruptedException,因為異常不能跨線程傳播回 main() 中,因此必須在本地進行處理。線程中拋出的其它異常也同樣需要在本地進行處理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
      

3、yield()

對靜態方法 Thread.yield() 的調用聲明瞭當前線程已經完成了生命周期中最重要的部分,可以切換給其它線程來執行。該方法只是對線程調度器的一個建議,而且也只是建議具有相同優先順序的其它線程可以運行。

public void run() {
    Thread.yield();
}
  

4、join()

一旦這個線程執行了這個方法,只有這個線程處於死亡狀態其他線程才能執行。

public class MyThread extends Thread {
11
12     public MyThread() {
13     }
14
15     public MyThread(String name) {
16         super(name);
17     }
18
19     @Override
20     public void run() {
21         for (int i = 0; i < 10; i++) {
22             System.out.println(getName() + ":" + i);
23         }
24     }
25
26     public static void main(String[] args) {
27         // 1.創建MyThread類的對象
28         MyThread myThread1 = new MyThread("線程1");
29         MyThread myThread2 = new MyThread("線程2");
30         MyThread myThread3 = new MyThread("線程3");
31
32         // 2.啟動線程
33         myThread1.start();
34         try {
35             // 等待myThread1線程死亡,只有當該線程死亡之後才能繼續執行其它線程
36             myThread1.join();
37         } catch (InterruptedException e) {
38             e.printStackTrace();
39         }
40         myThread2.start();
41         myThread3.start();
42
43     }
44 }

5、wait()\notify()

wait\notify\notifyAll操作都是屬於Object類提供的方法,即所有的對象都具有該方法,他們是的一對的,調用的時候不能分開呦。

wait():調用wait方法的線程,當前持有鎖的該線程等待,直至該對象的另一個持鎖線程調用notify/notifyAll操作。
wait(long timeOut)、wait(long timeOut,int nanos)

線程狀態轉換是,當wait被喚醒或超時,並不是直接進入到運行或者就緒狀態,而是先進入到Block狀態,搶鎖成功後,才能進入到可運行狀態。

wait方法在調用進入阻塞之前會釋放鎖,而sleep或join是不會釋放鎖的

notify():通知持有該對象鎖的所有線程中的的隨意一個線程被喚醒

notifyAll():通知持有該對象鎖的所有線程被同時喚醒

我們形象的做一個比喻:

如果把多線程比喻成一個運動員,跑道就是CPU每次只能允許一個運動員進入跑道,運動員的後勤保障就是守護進程,通過setDaemon()方法,運動員就轉業為了後勤人員。

執行sleep()就是提前設定一個時間,讓運動員休息會。wait()方法是運動員無限期的睡著,直到教練殺出來一腳踹醒(執行notify方法)運動員才會喚醒。

yield()會把跑道讓給別的運動員。

join()方法會讓運動員擁有最高的跑道許可權,我不跑完,誰都不能進來。

四、線程同步

Java允許併發控制,當多個線程同時操作一個可共用的資源變數時(如數據的增刪改查), 將會導致數據不准確,相互之間產生衝突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用, 從而保證了該變數的唯一性和準確性。

Java 提供了兩種鎖機制來控制多個線程對共用資源的互斥訪問,第一個是 JVM 實現的 synchronized,而另一個是 JDK 實現的 ReentrantLock。

1、synchronized

①. 同步一個代碼塊

public void func() {
    synchronized (this) {
        // ...
    }
}
  
      

它只作用於同一個對象,如果調用兩個對象上的同步代碼塊,就不會進行同步。

對於以下代碼,使用 ExecutorService 執行了兩個線程,由於調用的是同一個對象的同步代碼塊,因此這兩個線程會進行同步,當一個線程進入同步語句塊時,另一個線程就必須等待。

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
  
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}        
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
  
       

對於以下代碼,兩個線程調用了不同對象的同步代碼塊,因此這兩個線程就不需要同步。從輸出結果可以看出,兩個線程交叉執行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
        
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9  
     

②. 同步一個方法

public synchronized void func () {
    // ...
}
         

它和同步代碼塊一樣,作用於同一個對象。

③. 同步一個類

public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}  
       

作用於整個類,也就是說兩個線程調用同一個類的不同對象上的這種同步語句,也會進行同步。

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
  
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
  
    

④. 同步一個靜態方法

public synchronized static void fun() {
    // ...
}
  
    

作用於整個類。

2、ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖。

public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
        }
    }
}
   
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
     
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9    

3、比較

①. 鎖的實現**

synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。

②. 性能

新版本 Java 對 synchronized 進行了很多優化,例如自旋鎖等,synchronized 與 ReentrantLock 大致相同。

③. 等待可中斷

當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情。

ReentrantLock 可中斷,而 synchronized 不行。

④. 公平鎖

公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。

synchronized 中的鎖是非公平的,ReentrantLock 預設情況下也是非公平的,但是也可以是公平的。

⑤. 鎖綁定多個條件

一個 ReentrantLock 可以同時綁定多個 Condition 對象。

4、使用選擇

除非需要使用 ReentrantLock 的高級功能,否則優先使用 synchronized。

這是因為 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。

並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因為 JVM 會確保鎖的釋放。

 如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。

線程池就應用而生。

五、線程池

線程池圍繞著一個核心的類 java.uitl.concurrent.ThreadPoolExecutor,我們將它作為一個切入點揭開線程池的面紗。

1、核心線程類

 java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類,因此如果要透徹地瞭解Java中的線程池,必須先瞭解這個類。下麵我們來看一下ThreadPoolExecutor類的具體實現源碼。

在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:核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,預設情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。預設情況下,在創建了線程池後,線程池中的線程數為0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;

  • maximumPoolSize:線程池最大線程數,這個參數也是一個非常重要的參數,它表示線上程池中最多能創建多少個線程;

  • keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。預設情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime才會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閑的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,線上程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數為0;

  • unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:

    TimeUnit.DAYS;               //天
    TimeUnit.HOURS;             //小時
    TimeUnit.MINUTES;           //分鐘
    TimeUnit.SECONDS;           //秒
    TimeUnit.MILLISECONDS;      //毫秒
    TimeUnit.MICROSECONDS;      //微妙
    TimeUnit.NANOSECONDS;       //納秒
    
  • workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,觀察傳入的workQueue 都是預設,即最大可添加Integer.MAX_VALUE個任務,所有在使用過程中要避免使用預設線程池。這裡的阻塞隊列有以下幾種選擇:

    ArrayBlockingQueue;
    LinkedBlockingQueue;
    SynchronousQueue;
    
    ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
    
  • threadFactory:線程工廠,主要用來創建線程;

  • handler:表示當拒絕處理任務時的策略,有以下四種取值:

    ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
    ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
    ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重覆此過程)
    ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務 
    

以上對構造的七個參數進行了介紹,那麼這些參數是怎麼起作用的呢,我們接著看線程池的執行流程。

2、線程執行流程

  1. 當線程池小於corePoolSize時,新提交任務將創建一個新線程執行任務,即使此時線程池中存在空閑線程。
  2. 當線程池達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行
  3. 當workQueue已滿,且maximumPoolSize>corePoolSize時,新提交任務會創建新線程執行任務
  4. 當提交任務數超過maximumPoolSize時,新提交任務由RejectedExecutionHandler處理
  5. 當線程池中超過corePoolSize線程,空閑時間達到keepAliveTime時,釋放空閑線程
  6. 當設置allowCoreThreadTimeOut(true)時,該參數預設false,線程池中corePoolSize線程空閑時間達到keepAliveTime也將關閉

3、四種線程池及使用場景

Java通過Executors提供四種線程池,分別為

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

newCachedThreadPool:

  • 底層:返回ThreadPoolExecutor實例,corePoolSize為0;maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為60L;時間單位TimeUnit.SECONDS;workQueue為SynchronousQueue(同步隊列)

  • 通俗:當有新任務到來,則插入到SynchronousQueue中,由於SynchronousQueue是同步隊列,因此會在池中尋找可用線程來執行,若有可以線程則執行,若沒有可用線程則創建一個線程來執行該任務;若池中線程空閑時間超過指定時間,則該線程會被銷毀。

  • 適用:執行很多短期的非同步任務

    /**
     * 1.創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閑(60秒不執行任務)的線程<br>
     * 2.當任務數增加時,此線程池又可以智能的添加新線程來處理任務<br>
     * 3.此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小<br>
     */
    public static void cacheThreadPool() {
            ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 1; i <= 10; i++) {
                final int ii = i;
                try {
                    Thread.sleep(ii * 1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                cachedThreadPool.execute(()->out.println("線程名稱:" + Thread.currentThread().getName() + ",執行" + ii));
            }
    }
    -----output------
    線程名稱:pool-1-thread-1,執行1
    線程名稱:pool-1-thread-1,執行2
    線程名稱:pool-1-thread-1,執行3
    線程名稱:pool-1-thread-1,執行4
    線程名稱:pool-1-thread-1,執行5
    線程名稱:pool-1-thread-1,執行6
    線程名稱:pool-1-thread-1,執行7
    線程名稱:pool-1-thread-1,執行8
    線程名稱:pool-1-thread-1,執行9
    線程名稱:pool-1-thread-1,執行10
    

newFixedThreadPool:

  • 底層:返回ThreadPoolExecutor實例,接收參數為所設定線程數量n,corePoolSize和maximumPoolSize均為n;keepAliveTime為0L;時間單位TimeUnit.MILLISECONDS;WorkQueue為:new LinkedBlockingQueue() 無界阻塞隊列

  • 通俗:創建可容納固定數量線程的池子,每個線程的存活時間是無限的,當池子滿了就不再添加線程了;如果池中的所有線程均在繁忙狀態,對於新任務會進入阻塞隊列中(無界的阻塞隊列)

  • 適用:執行長期任務

     /**
      * 1.創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小<br>
      * 2.線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那麼線程池會補充一個新線程<br>
      * 3.因為線程池大小為3,每個任務輸出index後sleep 2秒,所以每兩秒列印3個數字,和線程名稱<br>
      */
    public static void fixTheadPoolTest() {
            ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                final int ii = i;
                fixedThreadPool.execute(() -> {
                    out.println("線程名稱:" + Thread.currentThread().getName() + ",執行" + ii);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
    }
    ------output-------
    線程名稱:pool-1-thread-3,執行2
    線程名稱:pool-1-thread-1,執行0
    線程名稱:pool-1-thread-2,執行3
    線程名稱:pool-1-thread-3,執行4
    線程名稱:pool-1-thread-1,執行5
    線程名稱:pool-1-thread-2,執行6
    線程名稱:pool-1-thread-3,執行7
    線程名稱:pool-1-thread-1,執行8
    線程名稱:pool-1-thread-3,執行9
    

newSingleThreadExecutor:

  • 底層:FinalizableDelegatedExecutorService包裝的ThreadPoolExecutor實例,corePoolSize為1;maximumPoolSize為1;keepAliveTime為0L;時間單位TimeUnit.MILLISECONDS;workQueue為:new LinkedBlockingQueue() 無解阻塞隊列

  • 通俗:創建只有一個線程的線程池,當該線程正繁忙時,對於新任務會進入阻塞隊列中(無界的阻塞隊列)

  • 適用:按順序執行任務的場景

    /**  *創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行
     */
    public static void singleTheadPoolTest() {
            ExecutorService pool = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                final int ii = i;
                pool.execute(() -> out.println(Thread.currentThread().getName() + "=>" + ii));
            }
    }
    
    -----output-------
    

    線程名稱:pool-1-thread-1,執行0
    線程名稱:pool-1-thread-1,執行1
    線程名稱:pool-1-thread-1,執行2
    線程名稱:pool-1-thread-1,執行3
    線程名稱:pool-1-thread-1,執行4
    線程名稱:pool-1-thread-1,執行5
    線程名稱:pool-1-thread-1,執行6
    線程名稱:pool-1-thread-1,執行7
    線程名稱:pool-1-thread-1,執行8
    線程名稱:pool-1-thread-1,執行9

NewScheduledThreadPool:

  • 底層:創建ScheduledThreadPoolExecutor實例,該對象繼承了ThreadPoolExecutor,corePoolSize為傳遞來的參數,maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為0;時間單位TimeUnit.NANOSECONDS;workQueue為:new DelayedWorkQueue() 一個按超時時間升序排序的隊列

  • 通俗:創建一個固定大小的線程池,線程池內線程存活時間無限制,線程池可以支持定時及周期性任務執行,如果所有線程均處於繁忙狀態,對於新任務會進入DelayedWorkQueue隊列中,這是一種按照超時時間排序的隊列結構

  • 適用:執行周期性任務

    /**
     * 創建一個定長線程池,支持定時及周期性任務執行。延遲執行
     */
    public static void sceduleThreadPool() {
            ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
            Runnable r1 = () -> out.println("線程名稱:" + Thread.currentThread().getName() + ",執行:3秒後執行");
            scheduledThreadPool.schedule(r1, 3, TimeUnit.SECONDS);
            Runnable r2 = () -> out.println("線程名稱:" + Thread.currentThread().getName() + ",執行:延遲2秒後每3秒執行一次");
            scheduledThreadPool.scheduleAtFixedRate(r2, 2, 3, TimeUnit.SECONDS);
            Runnable r3 = () -> out.println("線程名稱:" + Thread.currentThread().getName() + ",執行:普通任務");
            for (int i = 0; i < 5; i++) {
                scheduledThreadPool.execute(r3);
            }
    }
    ----output------
    線程名稱:pool-1-thread-1,執行:普通任務
    線程名稱:pool-1-thread-5,執行:普通任務
    線程名稱:pool-1-thread-4,執行:普通任務
    線程名稱:pool-1-thread-3,執行:普通任務
    線程名稱:pool-1-thread-2,執行:普通任務
    線程名稱:pool-1-thread-1,執行:延遲2秒後每3秒執行一次
    線程名稱:pool-1-thread-5,執行:3秒後執行
    線程名稱:pool-1-thread-4,執行:延遲2秒後每3秒執行一次
    線程名稱:pool-1-thread-4,執行:延遲2秒後每3秒執行一次
    線程名稱:pool-1-thread-4,執行:延遲2秒後每3秒執行一次
    線程名稱:pool-1-thread-4,執行:延遲2秒後每3秒執行一次
    

5、使用實例

在ThreadPoolTaskExecutor的原理章節中,有一系列的方法,如果我們手動調用這些線程池方法實現方法是極其複雜的。

①、在java中的使用

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){
             MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+
             executor.getQueue().size()+",已執行玩別的任務數目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}
 
 
class MyTask implements Runnable {
    private int taskNum;
     
    public MyTask(int num) {
        this.taskNum = num;
    }
     
    @Override
    public void run() {
        System.out.println("正在執行task "+taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"執行完畢");
    }
}

從執行結果可以看出,當線程池中線程的數目大於5時,便將任務放入任務緩存隊列裡面,當任務緩存隊列滿了之後,便創建新的線程。如果上面程式中,將for迴圈中改成執行20個任務,就會拋出任務拒絕異常了。

  不過在java doc中,並不提倡我們直接使用ThreadPoolExecutor,而是使用Executors類中提供的幾個靜態方法來創建線程池:

Executors.newCachedThreadPool();    //創建一個緩衝池,緩衝池容量大小為Integer.MAX_VALUE
Executors.newSingleThreadExecutor();  //創建容量為1的緩衝池
Executors.newFixedThreadPool(int);  //創建固定容量大小的緩衝池

 從它們的具體實現來看,它們實際上也是調用了ThreadPoolExecutor,只不過參數都已配置好了。

  newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;

  newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置為1,也使用的LinkedBlockingQueue;

  newCachedThreadPool將corePoolSize設置為0,將maximumPoolSize設置為Integer.MAX_VALUE,使用的SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閑超過60秒,就銷毀線程。

  實際中,如果Executors提供的三個靜態方法能滿足要求,就儘量使用它提供的三個方法,因為自己去手動配置ThreadPoolExecutor的參數有點麻煩,要根據實際任務的類型和數量來進行配置。

  另外,如果ThreadPoolExecutor達不到要求,可以自己繼承ThreadPoolExecutor類進行重寫。

②、在Spring中使用

以下為Java線程池在Spring中的使用,ThreadPoolTaskExecutor一個對象註入到Spring的容器中。

/**
 * 線程池配置
 *
 * @author tcy
 **/
@Configuration
public class ThreadPoolConfig {
  // 核心線程池大小
  private final int corePoolSize = 50;

  // 最大可創建的線程數
  private final int maxPoolSize = 200;

  // 隊列最大長度
  private final int queueCapacity = 1000;

  // 線程池維護線程所允許的空閑時間
  private final int keepAliveSeconds = 300;

  @Bean(name = "threadPoolTaskExecutor")
  public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(maxPoolSize);
    executor.setCorePoolSize(corePoolSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 線程池對拒絕任務(無線程可用)的處理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
  }

在方法或者類上加 @Async註解,標明該方法或類為多線程方法,Spirng內部會自動調用多線程的拒絕策略、線程初始化等方法。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 背景 在項目使用了Spring Security之後,很多介面無法訪問了,從瀏覽器的網路調試窗看到的是CORS的報錯和403的報錯 分析 我們先來看一下CORS是什麼,和它很相似的CSRF是什麼,在SpringSecurity中如何配置以及起的什麼作用 CORS(Cross Origin Resou ...
  • 本文我們來看下 SpringSecurity + JWT 實現單點登錄操作,本文 2W 字,預計閱讀時間 30 min,文章提供了代碼骨架,建議收藏。 一、什麼是單點登陸 單點登錄(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系 ...
  • 一、字元流的由來 由於使用位元組流操控中文時不是很方便,Java就提供了字元流來進行操控中文 實現原理:位元組流+編碼表 為什麼用位元組流進行複製帶有中文的文本文件時沒有問題? 因為底層操作會自動進行位元組拼接成中文 怎樣識別該位元組是中文呢? 漢字在存儲時,無論是UTF-8還是GBK,第一個位元組都是負數用來 ...
  • 《零基礎學Java》 文件輸入/輸出流 程式運行期間,大部分數據都被存儲在記憶體中,當程式結束或被關閉時,存儲在記憶體中的數據將會消失。如果要永久保存數據,那麼最好的辦法就是把數據保存到磁碟的文件中。為此,Java提供了文件輸入/輸出流,即 FilelnputStream類 與 FilcOutputSr ...
  • 之前刷到一個視頻,老師上課點到用系統點到回答問題,然後就點名結束了。相信很多學校現在也會玩這招吧,今天就用Python給大家做一個點名系統。來吧,展示… 一.準備工作 1.Tkinter Tkinter 是 python 內置的 TK GUI 工具集。TK 是 Tcl 語言的原生 GUI 庫。作為 ...
  • SpringCloud Function SpEL註入 漏洞分析 ...
  • 前言大家拍照的時候會用到全景嗎?在拍一個環境的時候還是會有很多人用全景的吧 ,今天教大家如何用Python拼接全景圖片。 圖像的全景拼接,即“縫合”兩張具有重疊區域的圖來創建一張全景圖。其中用到了電腦視覺和圖像處理技術有:關鍵點特征檢 測、局部不變特征、關鍵特征點匹配、RANSAC(Random ...
  • (Java 演算法的 ACM 模式) 前言 經常在 LeetCode 上用核心代碼模式刷題的小伙伴突然用 ACM 模式可能會適應不過來,把時間花在輸入輸出上很浪費時間,因此本篇筆記對 Java 演算法的 ACM 模式做了個小總結; 除此之外,需要註意一些小細節: 1. 數字讀取到字元串讀取間需要用 in ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...