Java 中的幾種線程池,你之前用對了嗎

来源:https://www.cnblogs.com/fengzheng/archive/2018/07/12/9297602.html
-Advertisement-
Play Games

好久不發文章了,難道是因為忙,其實是因為懶。這是一篇關於線程池使用和基本原理的科普水文,如果你經常用到線程池,不知道你的用法標準不標準,是否有隱藏的 OOM 風險。不經常用線程池的同學,還有對幾種線程的使用不甚瞭解的同學可以讀一下此文。 為什麼要使用線程池 雖然大家應該都已經很清楚了,但還是說一下。 ...


好久不發文章了,難道是因為忙,其實是因為懶。這是一篇關於線程池使用和基本原理的科普水文,如果你經常用到線程池,不知道你的用法標準不標準,是否有隱藏的 OOM 風險。不經常用線程池的同學,還有對幾種線程的使用不甚瞭解的同學可以讀一下此文。

為什麼要使用線程池

雖然大家應該都已經很清楚了,但還是說一下。其實歸根結底最主要的一個原因就是為了提高性能。

線程池和資料庫連接池是同樣的道理,資料庫連接池是為了減少連接建立和釋放帶來的性能開銷。而線程池則是為了減少線程建立和銷毀帶來的性能消耗。

以 web 項目為例,有以下兩種情況:

1、每次過來一個請求,都要在服務端創建一個新線程來處理請求,請求處理完成銷毀線程;

2、每次過來一個請求,服務端線上程池中直接拿過一個空閑的線程來處理這個請求,處理完成後還給線程池;

答案是肯定的,肯定是第二種使用線程池的方式性能更好。

除了性能這個最重要的原因外,線程池的使用可以幫助我們更合理的使用系統資源。還是以 web 項目為例,如果我們在服務端不使用線程池,而是無節制的來一個請求創建一個線程,系統資源將會很快被耗盡。而使用線程池的話,則可以防止這種情況發生,當然這要建立在正確合理的使用線程池的基礎上,要固定線程的最大數以及等待隊列的大小。

幾種線程池的使用和原理

線程池固然好用,但是要建立在正確的使用方式的基礎上,如果使用方式不當,同樣會出現問題。接下來就介紹一下幾種線程池的使用。

在大名鼎鼎的 J.U.C 包下已經提供了 Executors 類,它已經封裝實現了四種創建線程池的方式,它暴露出幾個簡單的方法供開發者調用。最終都是通過 new ThreadPoolExecutor() ExecutorService 實例,從而得到我們想要的線程池類型。這樣做其實有利有弊,好的是我們不用關心那麼多參數,只需要簡單的指定一兩個參數就可以;不好的是,這樣一來又屏蔽了很多細節,如果有些參數使用預設的,而開發者又不瞭解原理的情況下,可能會造成 OOM 等問題。

很多公司都不建議或者強制不允許直接使用 Executors 類提供的方法來創建線程池,例如阿裡巴巴Java開發手冊里就明確不允許這樣創建線程池,一定要通過 ThreadPoolExecutor(xx,xx,xx...) 來明確線程池的運行規則,指定更合理的參數。

先來看一下 ThreadPoolExecutor 的幾個參數和它們的意義,先來看一下它最完整參數的重載。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

一共有 7 個參數。

corePoolSize

核心線程數,當有任務進來的時候,如果當前線程數還未達到 corePoolSize 個數,則創建核心線程,核心線程有幾個特點:

1、當線程數未達到核心線程最大值的時候,新任務進來,即使有空閑線程,也不會復用,仍然新建核心線程;

2、核心線程一般不會被銷毀,即使是空閑的狀態,但是如果通過方法 allowCoreThreadTimeOut(boolean value) 設置為 true 時,超時也同樣會被銷毀;

3、生產環境首次初始化的時候,可以調用 prestartCoreThread() 方法來預先創建所有核心線程,避免第一次調用緩慢;

maximumPoolSize

除了有核心線程外,有些策略是當核心線程完全無空閑的時候,還會創建一些臨時的線程來處理任務,maximumPoolSize 就是核心線程 + 臨時線程的最大上限。臨時線程有一個超時機制,超過了設置的空閑時間沒有事兒乾,就會被銷毀。

keepAliveTime

這個就是上面兩個參數里所提到的超時時間,也就是線程的最大空閑時間,預設用於非核心線程,通過 allowCoreThreadTimeOut(boolean value) 方法設置後,也會用於核心線程。

unit

這個參數配合上面的 keepAliveTime ,指定超時的時間單位,秒、分、時等。

workQueue

等待執行的任務隊列,如果核心線程沒有空閑的了,新來的任務就會被放到這個等待隊列中。這個參數其實一定程度上決定了線程池的運行策略,為什麼這麼說呢,因為隊列分為有界隊列和無界隊列。

有界隊列:隊列的長度有上限,當核心線程滿載的時候,新任務進來進入隊列,當達到上限,有沒有核心線程去即時取走處理,這個時候,就會創建臨時線程。(警惕臨時線程無限增加的風險)

無界隊列:隊列沒有上限的,當沒有核心線程空閑的時候,新來的任務可以無止境的向隊列中添加,而永遠也不會創建臨時線程。(警惕任務隊列無限堆積的風險)

threadFactory

它是一個介面,用於實現生成線程的方式、定義線程名格式、是否後臺執行等等,可以用 Executors.defaultThreadFactory() 預設的實現即可,也可以用 Guava 等三方庫提供的方法實現,如果有特殊要求的話可以自己定義。它最重要的地方應該就是定義線程名稱的格式,便於排查問題了吧。

handler

當沒有空閑的線程處理任務,並且等待隊列已滿(當然這隻對有界隊列有效),再有新任務進來的話,就要做一些取捨了,而這個參數就是指定取捨策略的,有下麵四種策略可以選擇:

ThreadPoolExecutor.AbortPolicy:直接拋出異常,這是預設策略; 
ThreadPoolExecutor.DiscardPolicy:直接丟棄任務,但是不拋出異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後將新來的任務加入等待隊列
ThreadPoolExecutor.CallerRunsPolicy:由線程池所在的線程處理該任務,比如在 main 函數中創建線程池,如果執行此策略,將有 main 線程來執行該任務

雖然並不提倡用 Executors 中的方法來創建線程池,但還是用他們來講一下幾種線程池的原理。

1、newFixedThreadPool

它有兩個重載方法,代碼如下:

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

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

建立一個線程數量固定的線程池,規定的最大線程數量,超過這個數量之後進來的任務,會放到等待隊列中,如果有空閑線程,則在等待隊列中獲取,遵循先進先出原則。

創建固定線程數量線程池, corePoolSize 和 maximumPoolSize 要一致,即核心線程數和最大線程數(核心+非核心線程)一致,Executors 預設使用的是 LinkedBlockingQueue 作為等待隊列,這是一個無界隊列,這也是使用它的風險所在,除非你能保證提交的任務不會無節制的增長,否則不要使用無界隊列,這樣有可能造成等待隊列無限增加,造成 OOM。

正確的創建固定線程數線程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

public static ExecutorService createFixedThreadPool() {
        int poolSize = 5;
        int queueSize = 10;
        ExecutorService executorService = new ThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize), threadFactory, new ThreadPoolExecutor.AbortPolicy());
        return executorService;
    }

上面代碼是創建一個 5 個線程的固定數量線程池,這裡線程存活時間沒有作用,所以設置為 0,使用了 ArrayBlockingQueue 作為等待隊列,設置長度為 10 ,最多允許10個等待任務,超過的任務會執行預設的 AbortPolicy 策略,也就是直接拋異常。ThreadFactory 使用了 Guava 庫提供的方法,定義了線程名稱,方便之後排查問題。

2、newSingleThreadExecutor

建立一個只有一個線程的線程池,如果有超過一個任務進來,只有一個可以執行,其餘的都會放到等待隊列中,如果有空閑線程,則在等待隊列中獲取,遵循先進先出原則。使用 LinkedBlockingQueue 作為等待隊列。

這個方法同樣存在等待隊列無限長的問題,容易造成 OOM,所以正確的創建方式參考上面固定數量線程池創建的方式,只是把 poolSize 設置為 1 。

3、newCachedThreadPool

緩存型線程池,在核心線程達到最大值之前,有任務進來就會創建新的核心線程,並加入核心線程池,即時有空閑的線程,也不會復用。達到最大核心線程數後,新任務進來,如果有空閑線程,則直接拿來使用,如果沒有空閑線程,則新建臨時線程。並且線程的允許空閑時間都很短,如果超過空閑時間沒有活動,則銷毀臨時線程。關鍵點就在於它使用 SynchronousQueue 作為等待隊列,它不會保留任務,新任務進來後,直接創建臨時線程處理,這樣一來,也就容易造成無限制的創建線程,造成 OOM。

正確的創建緩存型線程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

    public static ExecutorService createCacheThreadPool(){
        int coreSize = 10;
        int maxSize = 20;
        return new ThreadPoolExecutor(coreSize, maxSize, 10L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), threadFactory, new ThreadPoolExecutor.AbortPolicy());
    }

4、newScheduledThreadPool

計劃型線程池,可以設置固定時間的延時或者定期執行任務,同樣是看線程池中有沒有空閑線程,如果有,直接拿來使用,如果沒有,則新建線程加入池。使用的是 DelayedWorkQueue 作為等待隊列,這中類型的隊列會保證只有到了指定的延時時間,才會執行任務。

正確的創建緩存型線程池的做法是

private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("fengzheng" + "-%d").setDaemon(true).build();

    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(2, threadFactory);
        executorService.scheduleAtFixedRate(task,0L,5L, TimeUnit.SECONDS);
        latch.await();
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "executing");
        }
    }

最後,各位同學不妨到我的公眾號里互動一下 : 古時的風箏 ,進入公眾號可以加入交流群

掃碼關註


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

-Advertisement-
Play Games
更多相關文章
  • 前言 在React項目的開發中經常會遇到這樣一個場景:嵌套組件與被嵌套組件的通信。 比如Tab組件啊,或者下拉框組件。 場景 這裡應用一個最簡單的Tab組件來呈現這個場景。 import React, { Component, PropTypes } from 'react' class Tab e ...
  • 組件有個屬性:hidden='' ,值為true/false ,當false的時候說明不隱藏,當true的時候說明隱藏,註意該隱藏是不保留組件位置的。 實現即 .js 配合.wxml 文件 一、在.js 文件下的 Page({}) 裡面 的data:{} 裡面 創建一個布爾類型的屬性 二、在.wxm ...
  • 1. 請求長度的限制 在HTTP協議中,從未規定GET/POST的請求長度限制,對於GET,對url的限制來源於瀏覽器或web伺服器,瀏覽器和伺服器限制了url的長度。因此,在使用GET請求時,傳輸數據會受到URL長度的限制。對於POST,由於沒有url傳值,理論上是不會受到限制的,但是實際上各個服 ...
  • 在我們的項目經常需要監聽一些鍵盤事件來觸發程式的執行,而Vue中允許在監聽的時候添加關鍵修飾符: 對於一些常用鍵,還提供了按鍵別名: 全部的按鍵別名: .enter .tab .delete (捕獲“刪除”和“退格”鍵) .esc .space .up .down .left .right 修飾鍵: ...
  • 問題:有兩個元素: A, B。兩則是嵌套關係,A是B的父節點。A和B都是塊元素。當在A上設置:margin: 0 auto的時候,B並沒有在頁面中居中。 margin: 0 auto 為什麼沒有生效? 解決:margin:0 auto;生效,需要一定的前提條件。 1 兩者是塊元素,即 display ...
  • 下麵是在變數開始的時候定義初始值 ...
  • Description 聰聰和可可是兄弟倆,他們倆經常為了一些瑣事打起來,例如家中只剩下最後一根冰棍而兩人都想吃、兩個人都想玩兒電腦(可是他們家只有一臺電腦)……遇到這種問題,一般情況下石頭剪刀布就好了,可是他們已經玩兒膩了這種低智商的游戲。他們的爸爸快被他們的爭吵煩死了,所以他發明瞭一個新游戲:由 ...
  • 我們在上一篇搭建了一個簡單的springboot應用,這一篇將介紹使用spring-data-jpa操作資料庫。 新建一個MySQL資料庫,這裡資料庫名為springboot,建立user_info數據表,作為我們示例操作的表對象。 user_info信息如下: 資料庫及表創建成功後,回到我們的工程 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...