成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入線程池的問題連環炮

開發 前端
線程的數量太少無法充分利用CPU,線程數太多的話會導致頻繁切換線程,上下文切換消耗資源,我們需要根據系統資源和業務性能來決定線程數量。

[[438988]]

這一篇是看了這一篇文章之后用于個人的學習記錄,加入了一些個人的理解,其中一些圖片也是來源于這篇文章https://mp.weixin.qq.com/s/NDOx94yY06OnHjrYq2lVYw

1、為什么會有線程池

JVM中的一個線程即對應一個操作系統的線程,也就是JVM的線程是由操作系統創建而來,創建線程和銷毀線程這些都需要操作系統來分別賦予資源和釋放資源等

也就意味著創建線程變成了一個比較重的操作

我們可以利用多線程去進行不同的工作,更高效的利用CPU資源,但是這并不意味著線程數量越多越好

我們的時代已經由原來的單核時代變成現在的多核時代了,這個核指的就是CPU,在原來的單核時代,如果一個線程一直是運算的邏輯過程,也就不涉及到線程的切換,因為這個線程一直在占用CPU,也就是屬于計算密集型

但是如果這個線程屬于IO密集型,也就是這個線程很多的時間都是在等待IO操作和處理IO操作,這樣就浪費了CPU這個大腦的處理能力了

于是就有了多線程,一個線程等待IO操作,另一個線程可以頂上,充分利用了CPU的資源

隨著多核時代的到來,對于這個CPU高效利用也就變得更加迫切,CPU的核心越來越多,能同時運行的線程數越來越多了,也就意味著此時的多線程并不只是去提高單核的處理能力,更是為了充分利用這個多核的大腦

但 CPU 的核心數有限,同時能運行的線程數有限,所以需要根據調度算法切換執行的線程,而線程的切換需要開銷,比如替換寄存器的內容、高速緩存的失效等等。

如果線程數太多,切換的頻率就變高,可能使得多線程帶來的好處抵不過線程切換帶來的開銷,得不償失。

因此線程的數量需要得以控制

2、什么是線程池

線程的數量太少無法充分利用CPU,線程數太多的話會導致頻繁切換線程,上下文切換消耗資源,我們需要根據系統資源和業務性能來決定線程數量

而線程的創建又是屬于一個比較重的操作,所以我們想到的就是緩存一批線程,這種思想大家都明白應該,就像是數據庫某張表需要經常查詢,造成DB壓力過大,我們就先把經常訪問訪問的數據放入到緩存中,用于緩解對于DB的訪問壓力

這個也是類似的道理,每次去新建和銷毀線程比較重,我們就可以通過緩存這些線程來減輕不必要的消耗

線程的數量我們需要根據硬件的資源和線程要執行的任務這些等綜合來決定

高并發、任務執行時間短的業務,線程池線程數可以設置為CPU核數+1,減少線程上下文的切換

并發不高、任務執行時間長的業務要分情況來討論

假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大線程池中的線程數目,讓CPU處理更多的業務

假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,線程數設置為CPU核數+1,線程池中的線程數設置得少一些,減少線程上下文的切換

并發高、業務執行時間長,解決這種類型任務的關鍵不在于線程池而在于整體架構的設計,看看這些業務里面某些數據是否能做緩存是第一步,增加服務器是第二步,至于線程池的設置,參考上面的設置即可。最后,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦。

大家應該都聽過對象池、連接池這些,池化的技術就是通過在池子里取出資源,然是使用完再放回到池子里,而線程池這一點稍微不太一樣,這里線程池相對來說更黑盒一些

不是我們從線程池中取線程使用,而是直接往線程池里扔任務,然后線程池幫我們去執行

3、實現線程池

線程池內部也是一個典型的生產者-消費者模型

線程池內部有一個存放任務列表的隊列,而內部會不斷的有線程去隊列中取任務來執行,用來消費

來看一個簡易版的線程池實現,這段代碼同樣來源于上面的博文

首先線程池內需要定義兩個成員變量,分別是阻塞隊列和線程列表,然后自定義線程使它的任務就是不斷的從阻塞隊列中拿任務然后執行。

  1. @Slf4j 
  2. public class YesThreadPool { 
  3.  
  4.  BlockingQueue<Runnable> taskQueue;  //存放任務的阻塞隊列 
  5.  List<YesThread> threads; //線程列表 
  6.  
  7.  YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) { 
  8.   this.taskQueue = taskQueue; 
  9.   threads = new ArrayList<>(threadSize); 
  10.   // 初始化線程,并定義名稱 
  11.   IntStream.rangeClosed(1, threadSize).forEach((i)-> { 
  12.    YesThread thread = new YesThread("yes-task-thread-" + i); 
  13.    thread.start(); 
  14.    threads.add(thread); 
  15.   }); 
  16.  } 
  17.  //提交任務只是往任務隊列里面塞任務 
  18. public void execute(Runnable task) throws InterruptedException { 
  19.   taskQueue.put(task); 
  20.  } 
  21.  
  22.  class YesThread extends Thread { //自定義一個線程 
  23.   public YesThread(String name) { 
  24.    super(name); 
  25.   } 
  26.   @Override 
  27.   public void run() { 
  28.    while (true) { //死循環 
  29.     Runnable task = null
  30.     try { 
  31.      task = taskQueue.take(); //不斷從任務隊列獲取任務 
  32.     } catch (InterruptedException e) { 
  33.      logger.error("記錄點東西.....", e); 
  34.     } 
  35.     task.run(); //執行 
  36.    } 
  37.   } 
  38.  } 

當然,這只是個最簡易版的,也有很多可以優化的點

4、線程池核心參數

第1個參數:設置核心線程數。默認情況下核心線程會一直存活

第2個參數:設置最大線程數。決定線程池最多可以創建的多少線程

第3個參數和第4個參數:用來設置線程空閑時間,和空閑時間的單位,當線程閑置超過空閑時間就會被銷毀。可以通過AllowCoreThreadTimeOut方法來允許核心線程被回收

第5個參數:設置緩沖隊列,圖中左下方的三個隊列是設置線程池時常使用的緩沖隊列

其中Array Blocking Queue是一個有界隊列,就是指隊列有最大容量限制。Linked Blocking Queue是無界隊列,就是隊列不限制容量。最后一個是Synchronous Queue,是一個同步隊列,內部沒有緩沖區

第6個參數:設置線程池工廠方法,線程工廠用來創建新線程,可以用來對線程的一些屬性進行定制,例如線程的Group、線程名、優先級等。一般使用默認工廠類即可

第7個參數:設置線程池滿時的拒絕策略

ThreadPoolExecutor默認有四個拒絕策略:

  • ThreadPoolExecutor.AbortPolicy() 直接拋出異常RejectedExecutionException,這個是默認的拒絕策略
  • ThreadPoolExecutor.CallerRunsPolicy() 直接在提交失敗時,由提交任務的線程直接執行提交的任務
  • ThreadPoolExecutor.DiscardPolicy() 直接丟棄后來的任務
  • ThreadPoolExecutor.DiscardOldestPolicy() 丟棄在隊列中最早提交的任務

5、線程池原理

我們向線程提交任務時可以使用Execute和Submit,區別就是Submit可以返回一個Future對象,通過Future對象可以了解任務執行情況,可以取消任務的執行,還可獲取執行結果或執行異常。Submit最終也是通過Execute執行的

線程池提交任務時的執行順序如下:

向線程池提交任務時,會首先判斷線程池中的線程數是否大于設置的核心線程數,如果不大于,就創建一個核心線程來執行任務。

如果大于核心線程數,就會判斷緩沖隊列是否滿了,如果沒有滿,則放入隊列,等待線程空閑時執行任務。

如果隊列已經滿了,則判斷是否達到了線程池設置的最大線程數,如果沒有達到,就創建新線程來執行任務。

如果已經達到了最大線程數,則執行指定的拒絕策略。這里需要注意隊列的判斷與最大線程數判斷的順序,不要搞反

線程池中的線程并不是一開始就將活躍線程直接拉滿的,而是隨著用的數量的增加,才會逐步增加線程的,這是一種懶加載思想

但是這里有一個靈魂問題,沒研究的小伙伴肯定是不知道的

6、當線程數小于活躍線程數的時候,并且線程數都處于空閑狀態,現在提交一個任務,是新起一個線程還是用之前的線程來執行該任務?

李老是這樣說的:

  1. If fewer than corePoolSize threads are running, try to start  
  2. a new thread with the given command as its first task. 

也就是無論其余線程是否空閑,只要此時線程數量小于核心線程數量,就會通過啟動一個線程來執行該任務

線程池是懶加載的,但是這里又顯得很勤快

也就是線程池是想要快速擁有核心線程數量的線程,這個作為線程池的中堅力量

而最大線程數其實是為了應付突發狀況。

舉個裝修的例子,正常情況下施工隊只要 5 個人去干活,這 5 人其實就是核心線程,但是由于工頭接的活太多了,導致 5 個人在約定工期內干不完,所以工頭又去找了 2 個人來一起干,所以 5 是核心線程數,7 是最大線程數。

平時就是 5 個人干活,特別忙的時候就找 7 個,等閑下來就會把多余的 2 個辭了

7、看到這里你可能會覺得核心線程在線程池里面會有特殊標記?

并沒有,不論是核心還是非核心線程,在線程池里面都是一視同仁,當淘汰的時候不會管是哪些線程,反正留下核心線程數個線程即可

8、你是怎么理解 KeepAliveTime 的?

線程池的重點是保留核心數量的線程,但是會預留一些線程來用于突發情況,當突發情況過去之后,還是只想保留核心線程,所以這個時候就通過這個時間來控制

當線程數量大于核心線程數量的時候,并且空閑時間超過KeepAliveTime的時候,就回收線程,直到線程數量和核心數量持平為止

看了上面的線程池的邏輯,不知道大家有沒有產生一個疑問

為什么要把任務先放在任務隊列里面,而不是把線程先拉滿到最大線程數?

這里我先說下我的個人理解

線程池的重點應該是核心線程池,而當線程數量不夠處理的時候,先放到隊列中也是屬于一種緩沖的思想,因為我們在設計核心線程數量的時候都是考慮的盡可能的最優的數量,所以重點也就變成了盡力去維持核心線程的數量

而隊列是可以自定義數量的,我們可以通過控制隊列的長度,來控制我們可以接受的任務堆積的程度,只有當任務堆積無法忍受的時候,才會繼續去啟動新的線程來執行這些任務

當我看了Yes大佬的看法之后,發現也是這樣理解的,但是解釋的更深一些,我來和大家解釋下

原生版線程池的實現可以認為是偏向 CPU 密集的,也就是當任務過多的時候不是先去創建更多的線程,而是先緩存任務,讓核心線程去消化,從上面的分析我們可以知道,當處理 CPU 密集型任務的時,線程太多反而會由于線程頻繁切換的開銷而得不償失,所以優先堆積任務而不是創建新的線程。

而像 Tomcat 這種業務場景,大部分情況下是需要大量 I/O 處理的情況就做了一些定制,修改了原生線程池的實現,使得在隊列沒滿的時候,可以創建線程至最大線程數。

9、如何修改原生線程池,使得可以先拉滿線程數再入任務隊列排隊?

這里的邏輯其實上面也說過了,大家看一下源碼就懂了,首先判斷的是工作線程是否小于核心線程,當工作線程小于核心線程時,直接增加線程數量來執行任務

當達到核心線程數量的時候,則判斷線程池是否在運行中,在運行中即執行入隊操作

接下來一起看看Tomcat實現線程池的邏輯

  1. public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor 

可以看到先繼承了 JUC 的線程池,然后我們重點關注一下 execute 這個方法

這里可以看到增加了submittedCount作為任務數的統計,統計所有未完成的任務數量

首先調用原生過程,如果捕獲到拒絕的異常,則判斷隊列類型,不正確,丟棄該任務,任務數量減一。

然后執行再次入隊列,試圖增加一次挽救的機會,入隊失敗,任務數量減一,最后處理捕獲異常,任務數量減一

然后我們再來看下代碼里出現的 TaskQueue,這個就是上面提到的定制關鍵點了。

可以看到這個任務隊列繼承了 LinkedBlockingQueue,并且有個 ThreadPoolExecutor 類型的成員變量 parent ,我們再來看下 offer 方法的實現,這里就是修改原來線程池任務提交與線程創建邏輯的核心了。

這里就是對于offer邏輯進行了加強,我們看一下

先是如果沒有線程實例,則直接按照原方法執行

接著判斷如果線程數量是最大線程數量,直接入隊

未完成的任務數小于線程數,證明此時還有閑著摸魚的線程,直接入隊即可,會自動消費

到最后,也就意味著此時核心線程都在運行,此時判斷線程數量是否小于最大線程數量,如果小于,這里就直接返回false即可,這個false就映射了上面ThreadPoolExecutor中的execute方法中的offer,然后便會執行相應的增加線程的操作,而不是先選擇入隊

10、原生線程池的核心線程一定要伴隨著任務慢慢創建嗎

既然這么問了,答案肯定是否定的,線程池中提供了

線程池提供了兩個方法:

  • prestartCoreThread:啟動一個核心線程
  • prestartAllCoreThreads :啟動所有核心線程

不要小看這個預創建方法,預熱很重要,不然剛重啟的一些服務有時是頂不住瞬時請求的,就立馬崩了,所以有預熱線程、緩存等等操作。

  1.  /** 
  2.     * Starts a core thread, causing it to idly wait for work. This 
  3.     * overrides the default policy of starting core threads only when 
  4.     * new tasks are executed. This method will return {@code false
  5.     * if all core threads have already been started. 
  6.     * @return {@code true} if a thread was started 
  7.  */ 
  8.  public boolean prestartCoreThread() { 
  9.       return workerCountOf(ctl.get()) < corePoolSize && 
  10.          addWorker(nulltrue); 
  11.  } 
  12.  /** 
  13.     * Starts all core threads, causing them to idly wait for work. This 
  14.     * overrides the default policy of starting core threads only when 
  15.     * new tasks are executed. 
  16.     * @return the number of threads started 
  17. */ 
  18.  public int prestartAllCoreThreads() { 
  19.      int n = 0; 
  20.      while (addWorker(nulltrue)) 
  21.          ++n; 
  22.      return n; 
  23.  } 

11、線程池的核心線程在空閑的時候一定不會被回收嗎?

有個allowCoreThreadTimeOut方法,把它設置為true ,則所有線程都會超時,不會有核心數那條線的存在。

12、線程池的關閉方法shutdown和shutdownNow

關閉線程池的方法,一個是安全的關閉線程池,會等待任務都執行完畢,一個是粗暴的直接咔嚓了所有線程,管你在不在運行,兩個方法分別調用的就是 interruptIdleWorkers() 和 interruptWorkers() 來中斷線程

  1. /** 
  2.    * Initiates an orderly shutdown in which previously submitted 
  3.    * tasks are executed, but no new tasks will be accepted. 
  4.    * Invocation has no additional effect if already shut down. 
  5.    * <p>This method does not wait for previously submitted tasks to 
  6.    * complete execution.  Use {@link #awaitTermination awaitTermination} 
  7.    * to do that. 
  8.    * @throws SecurityException {@inheritDoc} 
  9.   public void shutdown() { 
  10.       final ReentrantLock mainLock = this.mainLock; 
  11.       mainLock.lock(); 
  12.       try { 
  13.           checkShutdownAccess(); 
  14.           advanceRunState(SHUTDOWN); 
  15.           interruptIdleWorkers(); 
  16.           onShutdown(); // hook for ScheduledThreadPoolExecutor 
  17.       } finally { 
  18.           mainLock.unlock(); 
  19.       } 
  20.       tryTerminate(); 
  21.   } 
  22.  
  23. ** 
  24.    * Attempts to stop all actively executing tasks, halts the 
  25.    * processing of waiting tasks, and returns a list of the tasks 
  26.    * that were awaiting execution. These tasks are drained (removed) 
  27.    * from the task queue upon return from this method 
  28.    * <p>This method does not wait for actively executing tasks to 
  29.    * terminate.  Use {@link #awaitTermination awaitTermination} to 
  30.    * do that. 
  31.    * <p>There are no guarantees beyond best-effort attempts to stop 
  32.    * processing actively executing tasks.  This implementation 
  33.    * cancels tasks via {@link Thread#interrupt}, so any task that 
  34.    * fails to respond to interrupts may never terminate 
  35.    * @throws SecurityException {@inheritDoc} 
  36.    */ 
  37.   public List<Runnable> shutdownNow() { 
  38.       List<Runnable> tasks; 
  39.       final ReentrantLock mainLock = this.mainLock; 
  40.       mainLock.lock(); 
  41.       try { 
  42.           checkShutdownAccess(); 
  43.           advanceRunState(STOP); 
  44.           interruptWorkers(); 
  45.           tasks = drainQueue(); 
  46.       } finally { 
  47.           mainLock.unlock(); 
  48.       } 
  49.       tryTerminate(); 
  50.       return tasks; 
  51.   } 

這又可以引申出一個問題,shutdownNow 了之后還在任務隊列中的任務咋辦?眼尖的小伙伴應該已經看到了,線程池還算負責,把未執行的任務拖拽到了一個列表中然后返回,至于怎么處理,就交給調用者了

13、你肯定知道線程池里的 ctl 是干嘛的咯?

其實看下注釋就很清楚了,ctl 是一個涵蓋了兩個概念的原子整數類,它將工作線程數和線程池狀態結合在一起維護,低 29 位存放 workerCount,高 3 位存放 runState

其實并發包中有很多實現都是一個字段存多個值的,比如讀寫鎖的高 16 位存放讀鎖,低 16 位存放寫鎖,這種一個字段存放多個值可以更容易的維護多個值之間的一致性,也算是極簡主義

14、線程池有幾種狀態嗎?

注解說的很明白,我再翻譯一下:

RUNNING:能接受新任務,并處理阻塞隊列中的任務

SHUTDOWN:不接受新任務,但是可以處理阻塞隊列中的任務

STOP:不接受新任務,并且不處理阻塞隊列中的任務,并且還打斷正在運行任務的線程,就是直接撂擔子不干了!

TIDYING:所有任務都終止,并且工作線程也為0,處于關閉之前的狀態

TERMINATED:已關閉

15、線程池的狀態是如何變遷的嗎?

 

責任編輯:姜華 來源: Java賊船
相關推薦

2021-12-14 07:40:07

多線程面試CPU

2021-01-18 08:25:44

Zookeeper面試分布式

2021-01-19 09:11:35

Zookeeper面試分布式

2023-06-07 15:29:33

MySQL事務面試

2018-10-31 15:54:47

Java線程池源碼

2022-09-22 18:31:24

Kafka

2021-05-26 11:30:24

Java線程池代碼

2024-05-06 00:00:00

ThreadPool線程調度

2020-04-26 09:48:55

Redis面試

2023-12-29 09:38:00

Java線程池

2012-05-15 02:18:31

Java線程池

2025-02-24 08:00:00

線程池Java開發

2024-01-29 15:54:41

Java線程池公平鎖

2010-03-15 17:17:29

Java線程池

2021-01-06 14:15:42

線程池Java代碼

2020-05-14 17:41:40

Redis 6.0多線程數據庫

2023-05-19 08:01:24

Key消費場景

2021-09-11 15:26:23

Java多線程線程池

2023-11-29 16:38:12

線程池阻塞隊列開發

2015-08-20 09:17:36

Java線程池
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 黄色电影在线免费观看 | 国产精品视频导航 | 日韩精品一区二区三区中文在线 | 国产一级在线 | 国产黄色在线观看 | 日韩成人精品一区二区三区 | 久久久久久国产精品免费免费狐狸 | 国产日韩一区二区三区 | 欧美性受xxxx | 午夜免费福利电影 | 久久国产一区二区三区 | 国产亚洲一区二区三区在线观看 | 日韩综合在线 | 国产福利91精品 | 99re国产精品 | 成人午夜性成交 | 中文字幕免费视频 | 四虎影院在线观看免费视频 | 欧美精品久久久久久久久久 | 日韩伦理一区二区 | 久草免费在线视频 | 一区二区三区四区国产 | 天天草视频 | 中文在线www | 97操操 | 欧美综合一区二区三区 | 久久九九色 | 成人免费视频观看 | 五月婷婷丁香 | 久久久久久蜜桃一区二区 | 99视频在线 | 久久久国产精品视频 | 成人欧美一区二区三区黑人孕妇 | 久久免费精品视频 | 久热中文字幕 | 久久国产精品视频 | 日韩一区二区三区av | 久久91av| 成人福利 | 自拍偷拍中文字幕 | 精品在线一区 |