多執行緒【執行緒池】

跑調大叔!發表於2021-02-20

一、什麼是執行緒池

執行緒池:指在初始化一個多執行緒應用程式過程中建立一個執行緒集合,然後在需要執行新的任務時重用這些執行緒而不是新建一個執行緒,

一旦任務已經完成了,執行緒回到池子中並等待下一次分配任務。

二、使用執行緒池的好處

1)控制最大並大數。

2)降低資源消耗。通過重複利用已建立的執行緒來降低執行緒建立和銷燬造成的消耗。

3)提高響應速度。當任務到達時,任務不需要等到執行緒建立,而是可以直接使用執行緒池中的空閒執行緒。

4)提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配、延時執行、調優和監控等。

三、涉及到的類和介面

常用的執行緒池介面和類都在 java.util.concurrent包下,大致為:

Executor:執行緒池的頂級介面

ExecutorService:執行緒池介面,可通過submit()方法提交任務程式碼

ExecutorService介面的實現類最常用的為以下兩個:

ThreadPoolExecutor
ScheduledThreadPoolExecutor

和 Array -> Arrays、Collection -> Collections 一樣,執行緒池的建立也是有工具類可以使用的:

Executors工廠類:通過此類可以建立一個執行緒池

四、執行緒池種類

在 JDK 8 以後,一共有 5 種執行緒池,分別為:

固定執行緒數的執行緒池

只有一個執行緒的執行緒池

可根據任務數動態擴容執行緒數的執行緒池

可排程的執行緒池

具有搶佔式操作的執行緒池

這些執行緒池都能由 Executors 工具類來進行建立,分別對應以下方法:

1)newFixedThreadPool:建立指定的、固定個數的執行緒池
2)newCachedThreadPool:建立快取執行緒池(執行緒個數根據任務數逐漸增加,上線為 Integer.MAX_VALUE)
3)newSingleThreadExecutor:建立單個執行緒的執行緒池
4)newScheduledThreadPool:建立可排程的執行緒池 排程:定時、週期執行

5)newWorkStealingPool:建立具有搶佔式操作的執行緒池

對於 newWorkStealingPool 的補充:

newWorkStealingPool,這個是 JDK1.8 版本加入的一種執行緒池,stealing 翻譯為搶斷、竊取的意思,它實現的一個執行緒池和上面4種都不一樣,用的是 ForkJoinPool 類。

newWorkStealingPool 適合使用在很耗時的操作,但是 newWorkStealingPool 不是 ThreadPoolExecutor 的擴充套件,它是新的執行緒池類 ForkJoinPool 的擴充套件,但是都是在統一的一個 Executors 類中實現,由於能夠合理的使用 CPU 進行任務操作(並行操作),所以適合使用在很耗時的任務中

參考文章:

https://blog.csdn.net/qq_38428623/article/details/86689800
https://blog.csdn.net/tjbsl/article/details/98480843

五、如何使用執行緒池

(一)使用步驟

1)建立執行緒池物件

2)建立執行緒任務

3)使用執行緒池物件的 submit() 或者 execute() 方法提交要執行的任務

4)使用完畢,可以使用shutdown()方法關閉執行緒池

(二)案例程式碼

需求:使用執行緒池管理執行緒來簡單的模擬買票程式。

public class Demo(){
    public static void main(String[] args) {
        test();
    }
    
    public static void test(){
        //1、建立執行緒池物件
        ExecutorService pool = Executors.newFixedThreadPool(4);

        //2、建立任務
        Runnable runnable = new Runnable(){
            private int tickets = 100;

            @Override
            public void run() {
                while (true){
                    if(tickets <= 0){
                        break;
                    }
                    System.out.println(Thread.currentThread().getName()+"賣了第"+tickets+"張票");
                    tickets--;
                }
            }
        };
        
        //3、將任務提交到執行緒池(需要幾個執行緒來執行就提交幾次)
        for (int i=0; i<5; i++){
            pool.submit(runnable);
		}
        
        //4、關閉執行緒池
        pool.shutdown();
}

補充:

shutdown:啟動有序關閉,其中先前提交的任務將被執行,但不會接受任何新任務
shutdownNow:嘗試停止所有正在執行的任務,停止等待任務的處理,並返回正在等待執行的任務列表。

execute() 和 submit() 的區別:

1)引數:execute 只能傳遞 Runnable;submit 既可以傳遞 Runnable,也可以傳遞 Callable

2)返回值:execute 沒有返回值;submit 有返回值,可以獲取 Callable 的返回結果

六、執行緒池底層原始碼檢視

newFixedThreadPool

newCachedThreadPool

newSingleThreadExecutor

newScheduledThreadPool

七、執行緒池7大引數

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:執行緒池中的常駐核心執行緒數

maximumPoolSize:執行緒池中能夠容納同時指向的最大執行緒數,此值必須大於等於1

keepAliveTime:多餘空閒執行緒的存活時間

(若執行緒池中當前執行緒數超過corePoolSize時,且空閒執行緒的空閒時間達到keepAliveTime時,多餘空閒執行緒會被銷燬,直到只剩下corePoolSize個執行緒為止)

TimeUnit:keepAliveTime 的時間單位

workQueue:任務佇列,被提交但尚未被執行的任務

ThreadFactory:執行緒工廠,用於建立執行緒,一般用預設的即可

RejectedExecutionHandler:拒絕策略,當任務太多來不及處理,如何拒絕任務

八、執行緒池底層工作原理

1)執行緒池剛建立時,裡面沒有一個執行緒。任務佇列是作為引數傳進來的。不過,就算佇列裡面有任務,執行緒池也不會馬上執行它們。

2)當呼叫 execute() 方法新增一個任務時,執行緒池會做如下判斷:

​ a) 如果正在執行的執行緒數量小於 corePoolSize,那麼馬上建立執行緒執行這個任務;

​ b) 如果正在執行的執行緒數量大於或等於 corePoolSize,那麼將這個任務放入佇列;

​ c) 如果這時候佇列滿了,而且正在執行的執行緒數量小於 maximumPoolSize,那麼還是要建立非核心執行緒立刻執行這個任務

​ d) 如果佇列滿了,而且正在執行的執行緒數量大於或等於 maximumPoolSize,那麼執行緒池會執行拒絕策略。

3)當一個執行緒完成任務時,它會從佇列中取下一個任務來執行。

4)當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,如果當前執行的執行緒數大於 corePoolSize,那麼這個執行緒就被停掉。所以執行緒池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。

九、執行緒池的4大拒絕策略

執行緒池中的執行緒已經用完了,無法繼續為新任務服務,同時,等待佇列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。

JDK 內建的拒絕策略如下:

1)AbortPolicy : 直接丟擲 RejectedExecutionException 異常,阻止系統正常執行。

2)CallerRunsPolicy : 該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫執行緒。

3)DiscardOldestPolicy : 丟棄佇列中等待最久的執行緒,然後把當前任務加入佇列中嘗試再次提交當前任務。

4)DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。

以上內建拒絕策略均實現了 RejectedExecutionHandler 介面,若以上策略仍無法滿足實際需要,完全可以自己擴充套件 RejectedExecutionHandler 介面。

十、執行緒池的實際使用

通過檢視 Executors 提供的預設執行緒池的底層原始碼後,我們會發現其有如下弊端:

1)FixedThreadPool 和 SingleThreadPool:

允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:

允許的建立執行緒數量為 Integer.MAX_VALUE,可能會堆積大量的執行緒,從而導致 OOM。

並且在《阿里巴巴Java開發手冊》中也有指出,執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式手動建立,這樣的處理方式能讓程式設計師更加明確執行緒池的允許規則,從而規避資源耗盡的風險。

小結:在實際開發中不會使用 Executors 建立,而是手動建立,自己指定引數。

十一、執行緒池的手動建立

以上的引數是隨手寫的,實際開發中引數的設定要根據業務場景以及伺服器配置來進行設定。

十二、執行緒池配置合理執行緒數

設定執行緒池的引數時,需要從以下 2 個方面進行考慮:

系統是 CPU 密集型?

系統是 IO 密集型?

(一)CPU 密集型

CPU 密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行。

那麼這種情況下,應該儘可能配置少的執行緒數量,從而減少執行緒之間的切換,讓其充分利用時間進行計算。

一般公式為:CPU核數 + 1 個執行緒的執行緒池。

可以通過以下程式碼來檢視伺服器的核數:

Runtime.getRuntime().availableProcessors()
(二)IO 密集型

IO 密集型的意思是該任務需要大量的 IO,即大量的阻塞。

那麼這種情況下會導致有大量的 CPU 算力浪費在等待上,所以需要多配置執行緒數。

在 IO 密集型情況下,瞭解到有兩種配置執行緒數的公式:

公式一:CPU核數/(1-阻塞係數),其中阻塞係數在 0.8-0.9 之間

如:8核CPU,可以設定為 8/(1-0.9)=80 個執行緒

公式二:CPU核數 * 2

執行緒數的設定參考文章:

http://mp.weixin.qq.com/s__biz=MzI5MzYzMDAwNw==&mid=2247488456&idx=4&sn=80ee015180d46f2bd5b26c166b7dab0a&chksm=ec6e6a90db19e3867f8ea9fd5da01c3378431d6dcf940eb9820c4ab917e05851471102515d17&scene=0&xtrack=1#rd

http://mp.weixin.qq.com/s__biz=MzU1MzUyMjYzNg==&mid=2247484319&idx=1&sn=6a22ad5e324562c900a66624239cc6eb&chksm=fbf0c73ccc874e2a5d0a9c9d8e030e426104a52247b11a0069662a3d41a775f8c5332aba3981&mpshare=1&scene=24&srcid=&sharer_sharetime=1591622673427&sharer_shareid=5d06ca706f31f4d058e964b8b7ccfcc9#rd

Java新手,若有錯誤,歡迎指正!

相關文章