自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

八個(gè)線程池最佳實(shí)踐與避坑指南

開發(fā) 前端
線程池和 ThreadLocal?共用,可能會(huì)導(dǎo)致線程從ThreadLocal?獲取到的是舊值/臟數(shù)據(jù)。這是因?yàn)榫€程池會(huì)復(fù)用線程對象,與線程對象綁定的類的靜態(tài)屬性 ThreadLocal? 變量也會(huì)被重用,這就導(dǎo)致一個(gè)線程可能獲取到其他線程的ThreadLocal 值。

1. 正確聲明線程池

線程池必須手動(dòng)通過 ThreadPoolExecutor 的構(gòu)造函數(shù)來聲明,避免使用Executors 類創(chuàng)建線程池,會(huì)有 OOM 風(fēng)險(xiǎn)。

Executors 返回線程池對象的弊端如下(后文會(huì)詳細(xì)介紹到):

  • FixedThreadPool 和 SingleThreadExecutor :使用的是無界的 LinkedBlockingQueue,任務(wù)隊(duì)列最大長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導(dǎo)致 OOM。
  • CachedThreadPool :使用的是同步隊(duì)列 SynchronousQueue, 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會(huì)創(chuàng)建大量線程,從而導(dǎo)致 OOM。
  • ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的無界的延遲阻塞隊(duì)列DelayedWorkQueue,任務(wù)隊(duì)列最大長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導(dǎo)致 OOM。

說白了就是:使用有界隊(duì)列,控制線程創(chuàng)建數(shù)量。

除了避免 OOM 的原因之外,不推薦使用 Executors提供的兩種快捷的線程池的原因還有:

  • 實(shí)際使用中需要根據(jù)自己機(jī)器的性能、業(yè)務(wù)場景來手動(dòng)配置線程池的參數(shù)比如核心線程數(shù)、使用的任務(wù)隊(duì)列、飽和策略等等。
  • 我們應(yīng)該顯示地給我們的線程池命名,這樣有助于我們定位問題。

2. 監(jiān)測線程池運(yùn)行狀態(tài)

你可以通過一些手段來檢測線程池的運(yùn)行狀態(tài)比如 SpringBoot 中的 Actuator 組件。

除此之外,我們還可以利用 ThreadPoolExecutor 的相關(guān) API 做一個(gè)簡陋的監(jiān)控。從下圖可以看出, ThreadPoolExecutor提供了獲取線程池當(dāng)前的線程數(shù)和活躍線程數(shù)、已經(jīng)執(zhí)行完成的任務(wù)數(shù)、正在排隊(duì)中的任務(wù)數(shù)等等。

圖片圖片

下面是一個(gè)簡單的 Demo。printThreadPoolStatus()會(huì)每隔一秒打印出線程池的線程數(shù)、活躍線程數(shù)、完成的任務(wù)數(shù)、以及隊(duì)列中的任務(wù)數(shù)。

/**
 * 打印線程池的狀態(tài)
 *
 * @param threadPool 線程池對象
 */
public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
    ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false));
    scheduledExecutorService.scheduleAtFixedRate(() -> {
        log.info("=========================");
        log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
        log.info("Active Threads: {}", threadPool.getActiveCount());
        log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
        log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
        log.info("=========================");
    }, 0, 1, TimeUnit.SECONDS);
}

3. 建議不同類別的業(yè)務(wù)用不同的線程池

很多人在實(shí)際項(xiàng)目中都會(huì)有類似這樣的問題:我的項(xiàng)目中多個(gè)業(yè)務(wù)需要用到線程池,是為每個(gè)線程池都定義一個(gè)還是說定義一個(gè)公共的線程池呢?

一般建議是不同的業(yè)務(wù)使用不同的線程池,配置線程池的時(shí)候根據(jù)當(dāng)前業(yè)務(wù)的情況對當(dāng)前線程池進(jìn)行配置,因?yàn)椴煌臉I(yè)務(wù)的并發(fā)以及對資源的使用情況都不同,重心優(yōu)化系統(tǒng)性能瓶頸相關(guān)的業(yè)務(wù)。

我們再來看一個(gè)真實(shí)的事故案例!

圖片圖片

上面的代碼可能會(huì)存在死鎖的情況,為什么呢?畫個(gè)圖給大家捋一捋。

試想這樣一種極端情況:假如我們線程池的核心線程數(shù)為 n,父任務(wù)(扣費(fèi)任務(wù))數(shù)量為 n,父任務(wù)下面有兩個(gè)子任務(wù)(扣費(fèi)任務(wù)下的子任務(wù)),其中一個(gè)已經(jīng)執(zhí)行完成,另外一個(gè)被放在了任務(wù)隊(duì)列中。由于父任務(wù)把線程池核心線程資源用完,所以子任務(wù)因?yàn)闊o法獲取到線程資源無法正常執(zhí)行,一直被阻塞在隊(duì)列中。父任務(wù)等待子任務(wù)執(zhí)行完成,而子任務(wù)等待父任務(wù)釋放線程池資源,這也就造成了 "死鎖"

圖片圖片

解決方法也很簡單,就是新增加一個(gè)用于執(zhí)行子任務(wù)的線程池專門為其服務(wù)(也就是所謂的線程隔離)。

4. 別忘記給線程池命名

初始化線程池的時(shí)候需要顯示命名(設(shè)置線程池名稱前綴),有利于定位問題。

默認(rèn)情況下創(chuàng)建的線程名字類似 pool-1-thread-n 這樣的,沒有業(yè)務(wù)含義,不利于我們定位問題。

給線程池里的線程命名通常有下面兩種方式:

1、利用 guava 的 ThreadFactoryBuilder

ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)

2、自己實(shí)現(xiàn) ThreadFactor

/**
 * 線程工廠,它設(shè)置線程名稱,有利于我們定位問題
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final ThreadFactory delegate;
    private final String name;

    /**
     * 創(chuàng)建一個(gè)帶名字的線程池生產(chǎn)工廠
     */
    public NamingThreadFactory(ThreadFactory delegate, String name) {
        this.delegate = delegate;
        this.name = name; // TODO consider uniquifying this
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = delegate.newThread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }

}

5. 正確配置線程池參數(shù)

我們先來看一下各種書籍和博客上一般推薦的配置線程池參數(shù)的方式,可以作為參考!

常規(guī)操作

很多人甚至可能都會(huì)覺得把線程池配置過大一點(diǎn)比較好!我覺得這明顯是有問題的。就拿我們生活中非常常見的一例子來說:并不是人多就能把事情做好,增加了溝通交流成本。你本來一件事情只需要 3 個(gè)人做,你硬是拉來了 6 個(gè)人,會(huì)提升做事效率嘛?我想并不會(huì)。 線程數(shù)量過多的影響也是和我們分配多少人做事情一樣,對于多線程這個(gè)場景來說主要是增加了上下文切換成本。不清楚什么是上下文切換的話,可以看我下面的介紹。

上下文切換:

多線程編程中一般線程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線程使用,這個(gè)過程就屬于一次上下文切換。概括來說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換回這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換。

上下文切換通常是計(jì)算密集型的。也就是說,它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時(shí)間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時(shí)間,事實(shí)上,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。

Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少。

類比于實(shí)現(xiàn)世界中的人類通過合作做某件事情,我們可以肯定的一點(diǎn)是線程池大小設(shè)置過大或者過小都會(huì)有問題,合適的才是最好。

  • 如果我們設(shè)置的線程池?cái)?shù)量太小的話,如果同一時(shí)間有大量任務(wù)/請求需要處理,可能會(huì)導(dǎo)致大量的請求/任務(wù)在任務(wù)隊(duì)列中排隊(duì)等待執(zhí)行,甚至?xí)霈F(xiàn)任務(wù)隊(duì)列滿了之后任務(wù)/請求無法處理的情況,或者大量任務(wù)堆積在任務(wù)隊(duì)列導(dǎo)致 OOM。這樣很明顯是有問題的,CPU 根本沒有得到充分利用。
  • 如果我們設(shè)置線程數(shù)量太大,大量線程可能會(huì)同時(shí)在爭取 CPU 資源,這樣會(huì)導(dǎo)致大量的上下文切換,從而增加線程的執(zhí)行時(shí)間,影響了整體執(zhí)行效率。

有一個(gè)簡單并且適用面比較廣的公式:

  • CPU 密集型任務(wù)(N+1): 這種任務(wù)消耗的主要是 CPU 資源,可以將線程數(shù)設(shè)置為 N(CPU 核心數(shù))+1。比 CPU 核心數(shù)多出來的一個(gè)線程是為了防止線程偶發(fā)的缺頁中斷,或者其它原因?qū)е碌娜蝿?wù)暫停而帶來的影響。一旦任務(wù)暫停,CPU 就會(huì)處于空閑狀態(tài),而在這種情況下多出來的一個(gè)線程就可以充分利用 CPU 的空閑時(shí)間。
  • I/O 密集型任務(wù)(2N): 這種任務(wù)應(yīng)用起來,系統(tǒng)會(huì)用大部分的時(shí)間來處理 I/O 交互,而線程在處理 I/O 的時(shí)間段內(nèi)不會(huì)占用 CPU 來處理,這時(shí)就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務(wù)的應(yīng)用中,我們可以多配置一些線程,具體的計(jì)算方法是 2N。

如何判斷是 CPU 密集任務(wù)還是 IO 密集任務(wù)?

CPU 密集型簡單理解就是利用 CPU 計(jì)算能力的任務(wù)比如你在內(nèi)存中對大量數(shù)據(jù)進(jìn)行排序。但凡涉及到網(wǎng)絡(luò)讀取,文件讀取這類都是 IO 密集型,這類任務(wù)的特點(diǎn)是 CPU 計(jì)算耗費(fèi)時(shí)間相比于等待 IO 操作完成的時(shí)間來說很少,大部分時(shí)間都花在了等待 IO 操作完成上。

拓展一下:

線程數(shù)更嚴(yán)謹(jǐn)?shù)挠?jì)算的方法應(yīng)該是:最佳線程數(shù) = N(CPU 核心數(shù))?(1+WT(線程等待時(shí)間)/ST(線程計(jì)算時(shí)間)),其中 WT(線程等待時(shí)間)=線程運(yùn)行總時(shí)間 - ST(線程計(jì)算時(shí)間)。

線程等待時(shí)間所占比例越高,需要越多線程。線程計(jì)算時(shí)間所占比例越高,需要越少線程。

我們可以通過 JDK 自帶的工具 VisualVM 來查看 WT/ST 比例。

CPU 密集型任務(wù)的 WT/ST 接近或者等于 0,因此, 線程數(shù)可以設(shè)置為 N(CPU 核心數(shù))?(1+0)= N,和我們上面說的 N(CPU 核心數(shù))+1 差不多。

IO 密集型任務(wù)下,幾乎全是線程等待時(shí)間,從理論上來說,你就可以將線程數(shù)設(shè)置為 2N(按道理來說,WT/ST 的結(jié)果應(yīng)該比較大,這里選擇 2N 的原因應(yīng)該是為了避免創(chuàng)建過多線程吧)。

公示也只是參考,具體還是要根據(jù)項(xiàng)目實(shí)際線上運(yùn)行情況來動(dòng)態(tài)調(diào)整。我在后面介紹的美團(tuán)的線程池參數(shù)動(dòng)態(tài)配置這種方案就非常不錯(cuò),很實(shí)用!

動(dòng)態(tài)線程池

美團(tuán)技術(shù)團(tuán)隊(duì)在《Java 線程池實(shí)現(xiàn)原理及其在美團(tuán)業(yè)務(wù)中的實(shí)踐》這篇文章中介紹到對線程池參數(shù)實(shí)現(xiàn)可自定義配置的思路和方法。

美團(tuán)技術(shù)團(tuán)隊(duì)的思路是主要對線程池的核心參數(shù)實(shí)現(xiàn)自定義可配置。這三個(gè)核心參數(shù)是:

  • corePoolSize : 核心線程數(shù)線程數(shù)定義了最小可以同時(shí)運(yùn)行的線程數(shù)量。
  • maximumPoolSize : 當(dāng)隊(duì)列中存放的任務(wù)達(dá)到隊(duì)列容量的時(shí)候,當(dāng)前可以同時(shí)運(yùn)行的線程數(shù)量變?yōu)樽畲缶€程數(shù)。
  • workQueue: 當(dāng)新任務(wù)來的時(shí)候會(huì)先判斷當(dāng)前運(yùn)行的線程數(shù)量是否達(dá)到核心線程數(shù),如果達(dá)到的話,新任務(wù)就會(huì)被存放在隊(duì)列中。

為什么是這三個(gè)參數(shù)?

如何支持參數(shù)動(dòng)態(tài)配置? 且看 ThreadPoolExecutor 提供的下面這些方法。

圖片圖片

格外需要注意的是corePoolSize, 程序運(yùn)行期間的時(shí)候,我們調(diào)用 setCorePoolSize()這個(gè)方法的話,線程池會(huì)首先判斷當(dāng)前工作線程數(shù)是否大于corePoolSize,如果大于的話就會(huì)回收工作線程。

另外,你也看到了上面并沒有動(dòng)態(tài)指定隊(duì)列長度的方法,美團(tuán)的方式是自定義了一個(gè)叫做 ResizableCapacityLinkedBlockIngQueue 的隊(duì)列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 關(guān)鍵字修飾給去掉了,讓它變?yōu)榭勺兊模?/p>

最終實(shí)現(xiàn)的可動(dòng)態(tài)修改線程池參數(shù)效果如下

圖片圖片

動(dòng)態(tài)配置線程池參數(shù)最終效果

如果我們的項(xiàng)目也想要實(shí)現(xiàn)這種效果的話,可以借助現(xiàn)成的開源項(xiàng)目:

  • Hippo-4 :一款強(qiáng)大的動(dòng)態(tài)線程池框架,解決了傳統(tǒng)線程池使用存在的一些痛點(diǎn)比如線程池參數(shù)沒辦法動(dòng)態(tài)修改、不支持運(yùn)行時(shí)變量的傳遞、無法執(zhí)行優(yōu)雅關(guān)閉。除了支持動(dòng)態(tài)修改線程池參數(shù)、線程池任務(wù)傳遞上下文,還支持通知報(bào)警、運(yùn)行監(jiān)控等開箱即用的功能。
  • Dynamic TP :輕量級動(dòng)態(tài)線程池,內(nèi)置監(jiān)控告警功能,集成三方中間件線程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通過 SPI 自定義實(shí)現(xiàn))。

6. 線程池使用的一些小坑

重復(fù)創(chuàng)建線程池的坑

線程池是可以復(fù)用的,一定不要頻繁創(chuàng)建線程池比如一個(gè)用戶請求到了就單獨(dú)創(chuàng)建一個(gè)線程池。

@GetMapping("wrong")
public String wrong() throws InterruptedException {
    // 自定義線程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy());

    //  處理任務(wù)
    executor.execute(() -> {
      // ......
    }
    return "OK";
}

出現(xiàn)這種問題的原因還是對于線程池認(rèn)識(shí)不夠,需要加強(qiáng)線程池的基礎(chǔ)知識(shí)。

Spring 內(nèi)部線程池的坑

使用 Spring 內(nèi)部線程池時(shí),一定要手動(dòng)自定義線程池,配置合理的參數(shù),不然會(huì)出現(xiàn)生產(chǎn)問題(一個(gè)請求創(chuàng)建一個(gè)線程)。

@Configuration
@EnableAsync
public class ThreadPoolExecutorConfig {

    @Bean(name="threadPoolExecutor")
    public Executor threadPoolExecutor(){
        ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
        int processNum = Runtime.getRuntime().availableProcessors(); // 返回可用處理器的Java虛擬機(jī)的數(shù)量
        int corePoolSize = (int) (processNum / (1 - 0.2));
        int maxPoolSize = (int) (processNum / (1 - 0.5));
        threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小
        threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大線程數(shù)
        threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 隊(duì)列程度
        threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY);
        threadPoolExecutor.setDaemon(false);
        threadPoolExecutor.setKeepAliveSeconds(300);// 線程空閑時(shí)間
        threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // 線程名字前綴
        return threadPoolExecutor;
    }
}

線程池和 ThreadLocal 共用的坑

線程池和 ThreadLocal共用,可能會(huì)導(dǎo)致線程從ThreadLocal獲取到的是舊值/臟數(shù)據(jù)。這是因?yàn)榫€程池會(huì)復(fù)用線程對象,與線程對象綁定的類的靜態(tài)屬性 ThreadLocal 變量也會(huì)被重用,這就導(dǎo)致一個(gè)線程可能獲取到其他線程的ThreadLocal 值。

不要以為代碼中沒有顯示使用線程池就不存在線程池了,像常用的 Web 服務(wù)器 Tomcat 處理任務(wù)為了提高并發(fā)量,就使用到了線程池,并且使用的是基于原生 Java 線程池改進(jìn)完善得到的自定義線程池。

當(dāng)然了,你可以將 Tomcat 設(shè)置為單線程處理任務(wù)。不過,這并不合適,會(huì)嚴(yán)重影響其處理任務(wù)的速度。

server.tomcat.max-threads = 1

解決上述問題比較建議的辦法是使用阿里巴巴開源的 TransmittableThreadLocal(TTL)。TransmittableThreadLocal類繼承并加強(qiáng)了 JDK 內(nèi)置的InheritableThreadLocal類,在使用線程池等會(huì)池化復(fù)用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能,解決異步執(zhí)行時(shí)上下文傳遞的問題。

責(zé)任編輯:武曉燕 來源: 一安未來
相關(guān)推薦

2024-08-13 08:48:50

2021-02-22 17:00:31

Service Mes微服務(wù)開發(fā)

2023-05-24 10:06:42

多云實(shí)踐避坑

2019-01-15 10:29:48

物聯(lián)網(wǎng)IOTIT

2011-01-13 15:37:25

vSphere備份

2023-08-03 00:06:21

2024-09-29 10:39:14

并發(fā)Python多線程

2024-10-10 09:46:18

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2018-01-20 20:46:33

2024-04-24 13:45:00

2024-04-03 12:30:00

C++開發(fā)

2021-02-26 00:46:11

CIO數(shù)據(jù)決策數(shù)字化轉(zhuǎn)型

2020-06-12 11:03:22

Python開發(fā)工具

2021-09-16 19:22:06

Java概念concurrent

2024-03-28 12:51:00

Spring異步多線程

2015-09-17 09:01:26

創(chuàng)業(yè)智能硬件

2023-11-01 15:32:58

2024-08-09 13:49:56

2021-05-08 12:30:03

Pythonexe代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)