??線程池的概念
線程池,是一種線程的使用模式,它為了降低線程使用中頻繁的創(chuàng)建和銷毀所帶來的資源消耗與代價。
通過創(chuàng)建一定數(shù)量的線程,讓他們時刻準備就緒等待新任務的到達,而任務執(zhí)行結(jié)束之后再重新回來繼續(xù)待命。
想象這么一個場景:
在學校附近新開了一家快遞店,老板很精明,想到一個與眾不同的辦法來經(jīng)營。店里沒有雇人,而是每次有業(yè)務來了,就現(xiàn)場找一名同學過來把快遞送了,然后解雇同學。這個類比我們平時來一個任務,起一個線程進行處理的模式。
很快老板發(fā)現(xiàn)問題來了,每次招聘 + 解雇同學的成本還是非常高的。老板還是很善于變通的,知道了為什么大家都要雇人了,所以指定了一個指標,公司業(yè)務人員會擴張到 3 個人,但還是隨著業(yè)務逐步雇人。于是再有業(yè)務來了,老板就看,如果現(xiàn)在公司還沒 3 個人,就雇一個人去送快遞,否則只是把業(yè)務放到一個本本上,等著 3 個快遞人員空閑的時候去處理。這個就是我們要帶出的線程池的模式
線程池最核心的設計思路:復用線程,平攤線程的創(chuàng)建與銷毀的開銷代價
相比于來一個任務創(chuàng)建一個線程的方式,使用線程池的優(yōu)勢體現(xiàn)在如下幾點:
- 避免了線程的重復創(chuàng)建與開銷帶來的資源消耗代價
- 提升了任務響應速度,任務來了直接選一個線程執(zhí)行而無需等待線程的創(chuàng)建
- 線程的統(tǒng)一分配和管理,也方便統(tǒng)一的監(jiān)控和調(diào)優(yōu)
提升了響應速度是怎么體現(xiàn)的呢?
這里博主和大家詳細說一下:
比如你去銀行取錢
有兩種方法:
方法一,我們自己可以在銀行的取款機自己取,注意這時候我們是自主的,就像程序里的“用戶態(tài)”。用戶態(tài)執(zhí)行的是程序員自己的代碼,我想干嘛就干嘛,想在取款機里取錢、存錢、查詢余額等都在我的掌控范圍內(nèi)
方法二,我們?nèi)ス衽_取錢,我們不能進入銀行,只能交給柜員,讓他執(zhí)行你給的命令,間接完成,就像程序里的“內(nèi)核態(tài)”。此時你你給銀行內(nèi)部人員發(fā)去了取錢的命令,注意此時她是立馬給你取錢嗎?她可能給同事閑聊幾句、可能喝口水、錘錘背、或者領導叫她,這時候就會耽擱,你就只能等著,非常的被動,辦理時間也變長了
此時呢,我們的線程池就像方法一,我們可以利用已經(jīng)存在的線程自己完成操作,而不是去重新創(chuàng)建。
??標準庫中的線程池
Executors 創(chuàng)建方式有以下幾個
newFixedThreadPool | 創(chuàng)建固定線程數(shù)的線程池 |
---|---|
newCachedThreadPool | 創(chuàng)建線程數(shù)目動態(tài)增長的線程池. |
newSingleThreadExecutor | 創(chuàng)建只包含單個線程的線程池 |
newScheduledThreadPool | 設定 延遲時間后執(zhí)行命令,或者定期執(zhí)行命令. 是進階版的 Timer |
注意上述方法是有返回值的
返回值類型為 ExecutorService
使用實例如下:
-
使用 Executors.newFixedThreadPool(10) 能創(chuàng)建出固定包含 10 個線程的線程池.
-
返回值類型為 ExecutorService
-
通過 ExecutorService.submit 可以注冊一個任務到線程池中
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestDemo1 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for(int i = 1; i < 100;i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("任務"+n);
}
});
}
}
}
結(jié)果展示:
??ThreadPoolExecutor 類
Executors 本質(zhì)上是 ThreadPoolExecutor 類的封裝.
我們也可以去
Java文檔
進行查找
我們在圖示區(qū)域找到java.util.concurrent包
這個包里面放的很多都與并法編程相關的類
接下來我們點擊并找到ThreadPoolExecutor類
我們對這個類往下翻我們就可以看到它提供了一些構(gòu)造方法
我們可以就看到ThreadPoolExecutor 提供了更多的可選參數(shù), 可以進一步細化線程池行為的設定,接下來博主帶大家看一下這些參數(shù)代表的意義(這里第四個構(gòu)造方法參數(shù)最多,所以這里講第四個構(gòu)造方法的參數(shù))
??corePoolSize與maximumPoolSize
-
corePoolSize代表的是核心線程數(shù)
-
maximumPoolSize代表的最大線程數(shù)
corePoolSize 指的是核心線程數(shù),線程池初始化時線程數(shù)默認為 0,當有新的任務提交后,會創(chuàng)建新線程執(zhí)行任務,如果不做特殊設置,此后線程數(shù)通常不會再小于 corePoolSize ,因為它們是核心線程,即便未來可能沒有可執(zhí)行的任務也不會被銷毀。
隨著任務量的增加,在任務隊列滿了之后,線程池會進一步創(chuàng)建新線程,最多可以達到 maximumPoolSize 來應對任務多的場景,如果未來線程有空閑,大于 corePoolSize 的線程會被合理回收。所以正常情況下,線程池中的線程數(shù)量會處在 corePoolSize 與 maximumPoolSize 的閉區(qū)間內(nèi)。
就好比一個公司有很多員工,有正式員工,還有零時工
這兩者的區(qū)別在于:
-
正式員工是一定會存在的,而零時工是可以隨時被辭退的
-
公司忙的時候,需要員工就招募一些,活兒少了之后一些零時工可能就會被辭退
整體的策略為:正式員工打底,零時工動態(tài)調(diào)節(jié)
接下來我們討論一個問題,實際開法當中,線程池中的線程數(shù)應該設置為多少合適?
答案為:不同的程序特點不同,此時設置的線程數(shù)也是不同的
這是為什么呢?
這里我們考慮兩個極端情況:
-
CPU密集型:每個線程要執(zhí)行的任務都是通過狂轉(zhuǎn)CPU來實現(xiàn),此時的線程數(shù)最多不超過CPU的核數(shù),此時,你設置的再大也沒有用。
-
IO密集型:每個線程干的工作就是等待IO(讀寫硬盤、讀寫網(wǎng)卡、等待用戶輸入·······),不吃CPU,此時這樣的線程處于阻塞狀態(tài),不參與CPU調(diào)度,這時候多搞一些線程都無所謂,不再受制于CPU核數(shù)。理論上來說,可以設成無窮大(實際上當然是不行的)
然而,咱們再實際開放當中并沒有程序符合這兩種理想模型
真實的程序,往往一部分要吃CPU,一部分要等待IO
具體這個程序,幾成工作量吃CPU,幾成工作量等待IO,我們并不確定
那么我們實際開發(fā)中應該子怎么設置呢?
設置后進行測試,選擇一個最優(yōu)的選擇
??keepAliveTime
當線程池中線程數(shù)量多于核心線程數(shù)時,而此時又沒有任務可做,線程池就會檢測線程的 keepAliveTime,如果超過規(guī)定的時間,無事可做的線程就會被銷毀,以便減少內(nèi)存的占用和資源消耗。
如果后期任務又多了起來,線程池也會根據(jù)規(guī)則重新創(chuàng)建線程,所以這是一個可伸縮的過程,比較靈活,我們也可以用setKeepAliveTime 方法動態(tài)改變 keepAliveTime 的參數(shù)值。
就相當于零時工如果閑下來太久,就會被辭退。setKeepAliveTime 可以設置空閑的時間
??ThreadFactory
ThreadFactory 實際上是一個線程工廠,它的作用是生產(chǎn)線程以便執(zhí)行任務。我們可以選擇使用默認的線程工廠,創(chuàng)建的線程都會在同一個線程組,并擁有一樣的優(yōu)先級,且都不是守護線程,我們也可以選擇自己定制線程工廠,以方便給線程自定義命名,不同的線程池內(nèi)的線程通常會根據(jù)具體業(yè)務來定制不同的線程名
??workQueue
workQueue是線程池的任務隊列,作為一種緩沖機制,線程池會把當下沒有處理的任務放入任務隊列中,由于多線程同時從任務隊列中獲取任務是并發(fā)場景,此時就需要任務隊列滿足線程安全的要求,所以線程池中任務隊列采用
BlockingQueue 來保障線程安全。常用的隊列主要有以下幾種:
-
LinkedBlockingQueue
LinkedBlockingQueue是一個無界緩存等待隊列。當前執(zhí)行的線程數(shù)量達到corePoolSize的數(shù)量時,剩余的元素會在阻塞隊列里等待。(所以在使用此阻塞隊列時maximumPoolSizes就相當于無效了),每個線程完全獨立于其他線程。生產(chǎn)者和消費者使用獨立的鎖來控制數(shù)據(jù)的同步,即在高并發(fā)的情況下可以并行操作隊列中的數(shù)據(jù)。
這個隊列需要注意的是,雖然通常稱其為一個無界隊列,但是可以人為指定隊列大小,而且由于其用于記錄隊列大小的參數(shù)是int類型字段,所以通常意義上的無界其實就是隊列長度為 Integer.MAX_VALUE,且在不指定隊列大小的情況下也會默認隊列大小為 Integer.MAX_VALUE。 -
SynchronousQueue
SynchronousQueue沒有容量,是無緩沖等待隊列,是一個不存儲元素的阻塞隊列,會直接將任務交給消費者,必須等隊列中的添加元素被消費后才能繼續(xù)添加新的元素。擁有公平(FIFO)和非公平(LIFO)策略,使用SynchronousQueue阻塞隊列一般要求maximumPoolSizes為無界(Integer.MAX_VALUE),避免線程拒絕執(zhí)行操作。 -
ArrayBlockingQueue
ArrayBlockingQueue是一個有界緩存等待隊列,可以指定緩存隊列的大小,當正在執(zhí)行的線程數(shù)等于corePoolSize時,多余的元素緩存在ArrayBlockingQueue隊列中等待有空閑的線程時繼續(xù)執(zhí)行,當ArrayBlockingQueue已滿時,加入ArrayBlockingQueue失敗,會開啟新的線程去執(zhí)行,當線程數(shù)已經(jīng)達到最大的maximumPoolSizes時,再有新的元素嘗試加入ArrayBlockingQueue時會報錯。 -
DelayedWorkQueue
DelayedWorkQueue 的特點是內(nèi)部元素并不是按照放入的時間排序,而是會按照延遲的時間長短對任務進行排序,內(nèi)部采用的是“堆”的數(shù)據(jù)結(jié)構(gòu)。之所以線程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 選擇 DelayedWorkQueue,是因為它們本身正是基于時間執(zhí)行任務的,而延遲隊列正好可以把任務按時間進行排序,方便任務的執(zhí)行。
??RejectedExecutionHandler handler
在使用線程池并且使用有界隊列的時候,如果隊列滿了,任務添加到線程池的時候就會有問題,那么這些溢出的任務,ThreadPoolExecutor為我們提供了拒絕任務的處理方式,以便在必要的時候按照我們的策略來拒絕任務
線程池拒絕任務的時機有以下兩種:
-
第一種情況是當我們調(diào)用 shutdown 等方法關閉線程池后,即便此時可能線程池內(nèi)部依然有沒執(zhí)行完的任務正在執(zhí)行,但是由于線程池已經(jīng)關閉,此時如果再向線程池內(nèi)提交任務,就會遭到拒絕。
-
第二種情況是線程池沒有能力繼續(xù)處理新提交的任務,也就是工作已經(jīng)非常飽和的時候。
線程池任務拒絕策略實現(xiàn)了 RejectedExecutionHandler 接口,JDK 中自帶了四種任務拒絕策略。分別是AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。其中AbortPolicy是ThreadPoolExecutor默認使用。
那他們分別代表是策略內(nèi)容是什么呢?
-
AbortPolicy(默認)
這種拒絕策略在拒絕任務時,會直接拋出一個類RejectedExecutionException 的RuntimeException,讓你感知到任務被拒絕了,于是你便可以根據(jù)業(yè)務邏輯選擇重試或者放棄提交等策略。 -
DiscardPolicy
這種拒絕策略正如它的名字所描述的一樣,當新任務被提交后直接被丟棄掉,也不會給你任何的通知,相對而言存在一定的風險,因為我們提交的時候根本不知道這個任務會被丟棄,可能造成數(shù)據(jù)丟失。 -
DiscardOldestPolicy
如果線程池沒被關閉且沒有能力執(zhí)行,則會丟棄任務隊列中的頭結(jié)點,通常是存活時間最長的任務,這種策略與第二種不同之處在于它丟棄的不是最新提交的,而是隊列中存活時間最長的,這樣就可以騰出空間給新提交的任務,但同理它也存在一定的數(shù)據(jù)丟失風險。 -
CallerRunsPolicy
相對而言它就比較完善了,當有新任務提交后,如果線程池沒被關閉且沒有能力執(zhí)行,則把這個任務交于提交任務的線程執(zhí)行,也就是誰提交任務,誰就負責執(zhí)行任務。這樣做主要有兩點好處:
??第一點新提交的任務不會被丟棄,這樣也就不會造成業(yè)務損失。
??第二點好處是,由于誰提交任務誰就要負責執(zhí)行任務,這樣提交任務的線程就得負責執(zhí)行任務,而執(zhí)行任務又是比較耗時的,在這段期間,提交任務的線程被占用,也就不會再提交新的任務,減緩了任務提交的速度,相當于是一個負反饋。在此期間,線程池中的線程也可以充分利用這段時間來執(zhí)行掉一部分任務,騰出一定的空間,相當于是給了線程池一定的緩沖期。
??模擬實現(xiàn)線程池
接下來我們簡單模擬實現(xiàn)一個簡單的線程池
-
創(chuàng)建MyThreadPool實現(xiàn)我們的線程池
-
使用阻塞隊列組織所有任務
-
構(gòu)造方法里創(chuàng)建相應大小的線程數(shù)
-
提供一個submit方法使用線程池里面的線程
代碼實現(xiàn)如下:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MyThreadPool {
// 此處不涉及到 "時間" , 此處只有任務, 就直接使用 Runnable 即可~~
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// n 表示線程的數(shù)量
public MyThreadPool(int n) {
// 在這里創(chuàng)建出線程.
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
// 注冊任務給線程池.
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
測試代碼:
public class TestDemo2 {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10);
for(int i = 1; i < 100;i++) {
int n = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("任務"+n);
}
});
}
}
}
測試結(jié)果:
??多線程初階總結(jié)
至此多線程初階一全部講完,這里做一個小小的總結(jié):
??保證線程安全的大致思路:
- 使用沒有共享資源的模型
- 適用共享資源只讀,不寫的模型
-
不需要寫共享資源的模型
-
使用不可變對象
- 直面線程安全(重點)
-
保證原子性
-
保證順序性
-
保證可見性
??對比線程和進程
??線程的優(yōu)點
-
創(chuàng)建一個新線程的代價要比創(chuàng)建一個新進程小得多
-
與進程之間的切換相比,線程之間的切換需要操作系統(tǒng)做的工作要少很多
-
線程占用的資源要比進程少很多
-
能充分利用多處理器的可并行數(shù)量
-
在等待慢速I/O操作結(jié)束的同時,程序可執(zhí)行其他的計算任務
-
計算密集型應用,為了能在多處理器系統(tǒng)上運行,將計算分解到多個線程中實現(xiàn)
-
I/O密集型應用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作
??進程與線程的區(qū)別
-
進程是系統(tǒng)進行資源分配和調(diào)度的一個獨立單位,線程是程序執(zhí)行的最小單位。
-
進程有自己的內(nèi)存地址空間,線程只獨享指令流執(zhí)行的必要資源,如寄存器和棧。
-
由于同一進程的各線程間共享內(nèi)存和文件資源,可以不通過內(nèi)核進行直接通信。
-
線程的創(chuàng)建、切換及終止效率更高文章來源:http://www.zghlxwxcb.cn/news/detail-734679.html
?總結(jié)
關于《【JavaEE初階】 線程池詳解與實現(xiàn)》就講解到這兒,感謝大家的支持,歡迎各位留言交流以及批評指正,如果文章對您有幫助或者覺得作者寫的還不錯可以點一下關注,點贊,收藏支持一下!文章來源地址http://www.zghlxwxcb.cn/news/detail-734679.html
到了這里,關于【JavaEE初階】 線程池詳解與實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!