理解java併發中的線程池

1.線程池的出現

任何程序的運行和都與要耗費資源,因爲程序是在操作系統上運行的,涉及到與操作系統的交互,比如我們常用的數據庫連接池,沒有使用池化技術之前,每一個鏈接的創建和關閉都需要耗費資源,現在運用池化技術,將鏈接的創建關閉交給池統一處理,就可以達到節約資源,減少系統消耗的目的,類似數據庫連接池,線程也有自己的線程池
一個線程池中包含許多準備運行的空閒線程,將Runnable對象交給線程池,當使用線程池時,就會調用其中的線程,執行run方法,當run方法執行完畢,線程不會死亡,而是在池中爲下一個請求提供服務
線程池除了能夠統一的管理諸多線程,減少資源消耗之外,還有一個原因是減少併發線程數目,創建大量線程會大大降低性能,甚至導致虛擬機崩潰,總結下來,使用線程池的好處有

  • 降低資源的消耗
  • 提高響應的速度
  • 方便管理
  • 線程複用、可以控制最大併發數、管理線程

2.Executor-構建線程池

java.lang.concurrent包下有一個Executor接口,也叫執行器,·它來管理我們的線程,這裏被管理的線程要實現callablerunnable接口,我們主要關注ExecutorService這個Executor的子接口,我們之後創建的線程池都是這個接口的實現類,在這裏我們梳理一下這些類和接口的關係:
在這裏插入圖片描述
其中ThreadPoolExecutor是線程池的核心實現類,我們後面會重點介紹,Executor的作用不止用來創建線程池,還有其他作用,整個Executor系列可以說是一個線程框架體系,這裏我們介紹它的創建線程池的作用

2.1Executors-併發工具類

Collection和數組都有的CollectionsArrays工具類相似,Executor也有它的工具類:Executors
在這裏插入圖片描述
前面我們說到了線程池,又介紹了Executor框架,現在我們通過這個工具類來創建線程池,這個類有許多靜態工廠方法來創建線程池,在java中有三種線程池:

  • public static ExecutorService newSingleThreadExecutor()創建只有一個線程的線程池
  • public static ExecutorService newFixedThreadPool(int nThreads)創建固定數量線程的線程池
  • public static ExecutorService newCachedThreadPool()創建一個會根據需要而創建新線程的線程池

這三種線程池都是ExecutorService的實現類:

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

爲了說明這三個線程的區別,我們分別建立三個Demo來演示這三種方法:

2.2newSingleThreadExecutor

創建只有一個線程的線程池

public class Demo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 0; i < 10; i++) { // 使用了線程池之後,使用線程池來創建線程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "執行任務");
                });
            }
    }catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 線程池用完,程序結束,關閉線程池
        threadPool.shutdown();
        }
    }
}

輸出結果:

pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務

Process finished with exit code 0

可以發現只有一個線程來順序的執行我們的十個任務

2.3newFixedThreadPool

public class Demo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 0; i < 10; i++) { // 使用了線程池之後,使用線程池來創建線程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "執行任務");
                });
            }
    }catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 線程池用完,程序結束,關閉線程池
        threadPool.shutdown();
        }
    }
}

執行結果:

pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-1執行任務
pool-1-thread-2執行任務
pool-1-thread-3執行任務
pool-1-thread-4執行任務
pool-1-thread-5執行任務

我們發現是5個固定的線程來執行了10個任務,每個線程都被使用

2.4newCachedThreadPool

public class Demo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 10; i++) { // 使用了線程池之後,使用線程池來創建線程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "執行任務");
                });
            }
    }catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 線程池用完,程序結束,關閉線程池
        threadPool.shutdown();
        }
    }
}

執行結果:

pool-1-thread-1執行任務
pool-1-thread-2執行任務
pool-1-thread-3執行任務
pool-1-thread-6執行任務
pool-1-thread-7執行任務
pool-1-thread-8執行任務
pool-1-thread-10執行任務
pool-1-thread-9執行任務
pool-1-thread-4執行任務
pool-1-thread-5執行任務

可以看出,該類型線程池會根據任務需要而創建線程

3.使用ThreadPoolExecutor創建線程池

3.1爲什麼不適用Executors

我們演示了使用工具類創建的三種線程池,我們進入newFixedThreadPool源碼:
在這裏插入圖片描述
可以發現通過工具類創建的線程池底層是用的ThreadPoolExecutor,在《阿里巴巴java開發手冊》種規定了用ThreadPoolExecutor而不是Executors創建線程池:
在這裏插入圖片描述

3.2源碼分析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;
    }

這是一個構造器,我們將重點放在這7個參數上,這7個參數貫穿了我們線程池的所有功能特點:

  • int corePoolSize 核心線程池大小
  • int maximumPoolSize 最大核心線程池大小
  • long keepAliveTime 超時了沒有人調用就會釋放
  • TimeUnit unit 超時單位
  • BlockingQueue< Runnable> workQueue 阻塞隊列
  • ThreadFactory threadFactory 線程工廠:創建線程的
  • RejectedExecutionHandler handle 拒絕策略

我們舉個例子來說明這些參數的作用:
在這裏插入圖片描述
這是一個銀行,1,2號窗口代表核心線程池,表示在做任務時必須活躍的兩個線程,就好比銀行中始終有兩個窗口是服務的,假如現在連兩個窗口正在服務,那麼又有其他顧客進來了,3,4,5窗口這時候是關閉的,顧客會進入候客區也就是阻塞隊列等待1,2號窗口也就是核心線程完成任務後處理他們的業務,要是候客區滿了,又有顧客進來,那麼這時候3,4,5號窗口就要工作了,也就是我們設置的最大核心線程數量全部工作,3,4,5號線程處理完業務後,等待一段時間發現沒有任務處理就會進去線程池等待下一次的任務被執行,這就是超超時等待,要是這時候隊列滿了,所有線程都處於工作狀態,又有其他任務等待被處理該怎麼辦,這時候參數中的拒絕策略會起到作用,根據拒絕策略來處理這些多的線程

3.3手動設置並創建線程池

有了上面的介紹,我們來自定義一個線程池

public class Demo {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2,5,3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        try {
            for (int i = 0; i < 10; i++) { // 使用了線程池之後,使用線程池來創建線程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "執行任務");
                });
            }
    }catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 線程池用完,程序結束,關閉線程池
        threadPool.shutdown();
        }
    }
}

我們設置了兩個核心線程數量和5個最大線程數量,以及容量爲3的阻塞隊列,3s的等待時長,我們設置了10個任務,按照我們的解釋,這時候所有線程都會執行任務

pool-1-thread-1執行任務
pool-1-thread-2執行任務
pool-1-thread-2執行任務
pool-1-thread-2執行任務
pool-1-thread-1執行任務
pool-1-thread-3執行任務
pool-1-thread-4執行任務
pool-1-thread-5執行任務

結果確實如此,我們改變任務數量爲5,也就是最大數量線程池

pool-1-thread-1執行任務
pool-1-thread-2執行任務
pool-1-thread-2執行任務
pool-1-thread-2執行任務
pool-1-thread-2執行任務

我們發現只有核心線程池在處理任務,這和我們想的一樣,這裏就做這兩種演示,其他功能可以自己試

3.4四種拒絕策略

在上面的演示中,我們用的是new ThreadPoolExecutor.DiscardOldestPolicy()來處理多餘的無法處理的線程,下面我們來介紹java線程池中的4種拒絕策略:

在這裏插入圖片描述

  • new ThreadPoolExecutor.AbortPolicy()任務數>最大線程數量會拋出異常: java.util.concurrent.RejectedExecutionException:
  • new ThreadPoolExecutor.CallerRunsPolicy()被原來的線程處理
  • new ThreadPoolExecutor.CallerRunsPolicy()任務數>最大線程數量不會拋出異常
  • new ThreadPoolExecutor.DiscardOldestPolicy() 隊列滿了,嘗試去和最早的競爭,也不會 拋出異常!

4.CPU密集型和IO密集型

池的最大的大小如何去設置?最大線程到底該如何定義?要回答這兩個問題先要對IO密集型,CPU密集型進行了解

  • CPU密集型:對應電腦或服務器CPU的處理器數量,幾核就設置最大線程數量爲幾,可以使效率達到最高
  • IO密集型:判斷你程序中十分耗IO的線程,比如一個程序有20個大型任務,我們就可以設置20個線程去執行這個程序
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章