wordpress實(shí)例站百度推廣托管公司
目錄
·前言
一、什么是線程池
1.引入線程池的原因
2.線程池的介紹
二、標(biāo)準(zhǔn)庫中的線程池
1.構(gòu)造方法
2.方法參數(shù)
(1)corePoolSize?與?maximumPoolSize?
(2)keepAliveTime?與?unit
(3)workQueue(任務(wù)隊(duì)列)
(4)threadFactory(線程工廠)
(5)?handler(拒絕策略)
3.使用標(biāo)準(zhǔn)庫中線程池
三、實(shí)現(xiàn)線程池
·結(jié)尾
·前言
? ? ? ? 在我們學(xué)習(xí)編程知識(shí)過程中一定聽說過很多池,比如常量池,還有在我前面?MySql?專欄中?JDBC 編程里提到的數(shù)據(jù)庫連接池,以及本篇文章要為大家介紹的線程池,所謂的這些池作用其實(shí)都差不多,都是提前把要用的對(duì)象創(chuàng)建好,然后把用完的對(duì)象不立即釋放留著以備下次使用,這樣就可以起到提高效率的作用,本篇文章就會(huì)為大家介紹一下什么是線程池,在我們?Java?標(biāo)準(zhǔn)庫中的線程池是什么樣的,以及使用?Java?代碼來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的線程池讓大家能更清晰的認(rèn)識(shí)線程池,那么就開始本篇文章的介紹內(nèi)容吧。
一、什么是線程池
1.引入線程池的原因
? ? ? ? 在我們最開始,引入進(jìn)程的概念就能夠解決并發(fā)編程的問題,后來由于頻繁創(chuàng)建銷毀進(jìn)程帶來的開銷太大,從而引入了線程(輕量級(jí)進(jìn)程)這樣的概念,使用復(fù)用資源的方式來提高創(chuàng)建銷毀的效率,但是如果創(chuàng)建和銷毀線程的頻率也進(jìn)一步提高呢?此時(shí),線程的創(chuàng)建和開銷也就不能無視了。
? ? ? ? 為了優(yōu)化線程的創(chuàng)建與銷毀的效率,有下面兩種解決方案:
- 引入輕量級(jí)線程,也稱為“協(xié)程”;
- 使用線程池。
? ? ? ? ?為什么協(xié)程可以優(yōu)化線程的開銷與銷毀,這是因?yàn)閰f(xié)程的本質(zhì)是我們?cè)谟脩魬B(tài)代碼中進(jìn)行調(diào)度,不是靠?jī)?nèi)核的調(diào)度器調(diào)度的,這樣就可以節(jié)省很多調(diào)度上的開銷,此時(shí),我們代碼中創(chuàng)建上千個(gè)線程會(huì)卡死,但是創(chuàng)建上千個(gè)協(xié)程就沒什么事了。
? ? ? ? 雖然協(xié)程有很多的好處,但是在?Java?中不是很推薦用上述做法來優(yōu)化線程的創(chuàng)建與銷毀,這是因?yàn)橐雲(yún)f(xié)程會(huì)引入額外的復(fù)雜性,使用協(xié)程可能不是很穩(wěn)定,協(xié)程的調(diào)試比較困難……所以相比于協(xié)程,使用線程池對(duì)于優(yōu)化線程的創(chuàng)建與銷毀會(huì)更好一些,那么下面就進(jìn)一步介紹一下線程池是什么吧。
2.線程池的介紹
? ? ? ? 線程池就是要把使用的線程提前創(chuàng)建好,用完了一個(gè)線程也不要直接釋放,而是放到線程池中以備下次的使用,這樣就節(jié)省了線程創(chuàng)建與銷毀的開銷,因?yàn)樵谶@使用線程的過程中,并沒有真的頻繁創(chuàng)建和銷毀線程,只是從線程池中取線程使用,用完還會(huì)放回去。
? ? ? ? 那么為什么從線程池中取線程就比從系統(tǒng)中申請(qǐng)更高效呢?這就好比你讓室友幫你取快遞,室友答應(yīng)幫你取,但是什么時(shí)候給你取回來,他在幫你取快遞的途中會(huì)不會(huì)做一些什么事情都是不確定的,相比之下,你自己去取快遞,就會(huì)更高效,通過上面的例子,我們可以得到以下結(jié)論:
- 從線程池中取線程是純用戶態(tài)代碼,是可控的;
- 通過系統(tǒng)申請(qǐng)創(chuàng)建線程,需要系統(tǒng)內(nèi)核來完成,這是不可控的。
二、標(biāo)準(zhǔn)庫中的線程池
1.構(gòu)造方法
? ? ? ? 在?Java?標(biāo)準(zhǔn)庫中?ThreadPoolExecutor?這個(gè)類就是用來創(chuàng)建線程池的,關(guān)于這個(gè)類,它的構(gòu)造方法有很多的參數(shù),由我來給大家介紹一下,下面是這個(gè)類的幾個(gè)構(gòu)造方法,如下圖所示:
? ? ? ? 如上圖,ThreadPoolExecutor?一共涉及到四個(gè)構(gòu)造方法,這里我只對(duì)第四個(gè)構(gòu)造方法的每個(gè)參數(shù)進(jìn)行一個(gè)介紹,這是因?yàn)?#xff0c;最后一個(gè)構(gòu)造方法的參數(shù)是最全的,可以這么理解,介紹完第四個(gè)構(gòu)造方法的各個(gè)參數(shù),其余三個(gè)構(gòu)造方法也就都包含了。
2.方法參數(shù)
(1)corePoolSize?與?maximumPoolSize?
? ? ? ? 在標(biāo)準(zhǔn)庫提供的線程池中,持有的線程個(gè)數(shù)并不是一成不變的,它會(huì)根據(jù)當(dāng)前的任務(wù)量來自適應(yīng)當(dāng)前線程的個(gè)數(shù)(任務(wù)數(shù)量很多,就會(huì)多創(chuàng)建幾個(gè)線程,任務(wù)量比較少,就會(huì)少創(chuàng)建幾個(gè)線程),在構(gòu)造方法中的前兩個(gè)參數(shù)?int?corePoolSize?代表線程池中核心線程數(shù)有多少也就是一個(gè)線程池中最少得有多少個(gè)線程,int?maximumPoolSize?代表了線程池中最大線程數(shù)是多少也就是一個(gè)線程池中最多能有多少個(gè)線程。
(2)keepAliveTime?與?unit
? ? ? ? 第三個(gè)參數(shù):long?keepAliveTime?代表的意思就是線程池中除了核心線程外的線程的保持存活時(shí)間,在上面介紹了標(biāo)準(zhǔn)庫中的線程池是根據(jù)當(dāng)前的任務(wù)量來自適應(yīng)當(dāng)前線程的個(gè)數(shù),這個(gè)參數(shù)就是自適應(yīng)實(shí)現(xiàn)的一個(gè)重要標(biāo)準(zhǔn),keepAliveTime?可以記錄除了核心線程外的線程空閑的時(shí)間,如果這些線程的空閑時(shí)間超過了?keepAliveTime 的值就會(huì)自動(dòng)銷毀這些線程,來達(dá)到一個(gè)自適應(yīng)的效果,這里的第四個(gè)參數(shù):TimeUnit?unit 就是搭配?keepAliveTime?這個(gè)參數(shù)的,unit?代表的是時(shí)間單位,它可以是 s、min、ms、hour……也就代表了空閑時(shí)間 keepAliveTime?的時(shí)間單位。
(3)workQueue(任務(wù)隊(duì)列)
? ? ? ? 第五個(gè)參數(shù):BlockingQueue了<Runnable> workQueue?代表線程池中可以有很多個(gè)任務(wù),這里使用?Runnable?來作為描述任務(wù)的主體,線程池中線程不斷從這個(gè)阻塞隊(duì)列中取任務(wù)來執(zhí)行。
(4)threadFactory(線程工廠)
? ? ? ? 第六個(gè)參數(shù):ThreadFactory?threadFactory?意思是線程工廠,通過這個(gè)工廠類就可以來創(chuàng)建線程對(duì)象(Thread?對(duì)象),這個(gè)類里提供了方法,方法中封裝了?new?Thread?這樣的操作,并且同時(shí)給?Thread?設(shè)置了一些屬性,由此就構(gòu)成了 ThreadFactory?線程工廠。?
? ? ? ? 這里的線程工廠也用到了一種設(shè)計(jì)模式:工廠模式,它是通過專門的“工廠類”/“工廠對(duì)象”來創(chuàng)建指定對(duì)象,那么為什么使用工廠模式呢?我們來看下面的一個(gè)代碼示例:
// 表示平面上的一個(gè)點(diǎn)
class Point {// 笛卡爾坐標(biāo)系 x 與 yprivate double x;private double y;// 極坐標(biāo)系 r 與 aprivate double r;private double a;// 通過笛卡爾坐標(biāo)系來構(gòu)造這個(gè)點(diǎn)public Point(double x, double y) {setX(x);setY(y);}// 通過極坐標(biāo)系來構(gòu)造這個(gè)點(diǎn)public Point(double r, double a) {setR(r);setA(a);}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}
? ? ? ? 不知道大家看完上面的代碼有沒有發(fā)現(xiàn)什么問題,這里的問題就在于?Point?這個(gè)類的兩個(gè)構(gòu)造方法不構(gòu)成重載,它們的參數(shù)列表是一樣的,如下圖所示:?
? ? ? ? 想必我們都知道,使用笛卡爾坐標(biāo)和使用極坐標(biāo)都可以表示一個(gè)點(diǎn),并且這兩個(gè)表示方法并不相同,想通過同一個(gè)類的構(gòu)造方法來用這兩種不同的方式表示不同的點(diǎn)就違背了 Java 的語法規(guī)則,為了解決上述的問題,就引入了“工廠模式”。
? ? ? ? 工廠模式的基本邏輯就是使用普通方法來創(chuàng)建對(duì)象,在普通方法中把構(gòu)造方法進(jìn)行封裝,利用工廠模式修改后的代碼及運(yùn)行結(jié)果如下所示:
// 表示平面上的一個(gè)點(diǎn)
class Point {// 笛卡爾坐標(biāo)系 x 與 yprivate double x;private double y;// 極坐標(biāo)系 r 與 aprivate double r;private double a;// 通過笛卡爾坐標(biāo)系來構(gòu)造這個(gè)點(diǎn)public static Point makePointByXY(double x, double y) {Point point = new Point();point.setX(x);point.setY(y);return point;}// 通過極坐標(biāo)系來構(gòu)造這個(gè)點(diǎn)public static Point makePointByRA(double r, double a) {Point point = new Point();point.setR(r);point.setA(a);return point;}public double x() {return x;}public void setX(double x) {this.x = x;}public double y() {return y;}public void setY(double y) {this.y = y;}public double r() {return r;}public void setR(double r) {this.r = r;}public double a() {return a;}public void setA(double a) {this.a = a;}
}
public class PointFactory {public static void main(String[] args) {Point point1 = Point.makePointByXY(3,4);Point point2 = Point.makePointByRA(3,30);System.out.println("point1 的笛卡爾坐標(biāo):->(" + point1.x() + "," + point1.y() + ")");System.out.println("point2 的極坐標(biāo):->(" + point2.r() + "," + point2.a() + ")");}
}
?
?????????此時(shí),利用工廠模式就可以創(chuàng)建出兩個(gè)方式表示的點(diǎn),代碼中的?makePointByXY?方法與?makePointByRA?方法也稱為工廠方法,如果把工廠方法放到一個(gè)其他的類中,這個(gè)類就叫做“工廠類”,總的來說,通過靜態(tài)方法封裝?new?操作,在方法內(nèi)部設(shè)定不同的屬性來完成對(duì)象的初始化,構(gòu)造對(duì)象的過程,就是工廠模式。
(5)?handler(拒絕策略)
? ? ? ? 第七個(gè)參數(shù):RejectedExecutionHandler?handler?這個(gè)參數(shù)可以算是最重要的一個(gè)參數(shù),在前面介紹的第五個(gè)參數(shù):BlockingQueue了<Runnable> workQueue?這是線程池中的一個(gè)阻塞隊(duì)列,用來存儲(chǔ)當(dāng)前線程池要執(zhí)行的任務(wù)都有哪些,它能夠容納的元素是有上限的,此時(shí)當(dāng)這個(gè)阻塞隊(duì)列中的任務(wù)已經(jīng)排滿了,還有新的任務(wù)要往這個(gè)阻塞隊(duì)列中添加,線程池該怎么辦?這就需要我們的第七個(gè)參數(shù):RejectedExecutionHandler?handler?來指明一個(gè)拒絕策略,如下圖所示:
? ? ? ? 上圖中的這四個(gè)類也就代表了四種拒絕策略,它們所對(duì)應(yīng)的拒絕策略如下表所示:
ThreadPoolExecutor.AbortPolicy | 繼續(xù)添加任務(wù),直接拋出異常。 |
ThreadPoolExecutor.CallerRunsPolicy | 新的任務(wù)由添加任務(wù)的線程負(fù)責(zé)執(zhí)行。 |
ThreadPoolExecutor.DiscardOldestPolicy | 丟棄最老的任務(wù),添加新的任務(wù)。 |
ThreadPoolExecutor.DiscardPolicy | 丟棄最新的任務(wù)。 |
3.使用標(biāo)準(zhǔn)庫中線程池
? ? ? ? 上面介紹了?ThreadPoolExecutor?類的構(gòu)造方法及構(gòu)造方法中的參數(shù),可以看出來?ThreadPoolExecutor?類本身用起來比較復(fù)雜,因此在標(biāo)準(zhǔn)庫中還提供了另一個(gè)版本的線程池,也就是把?ThreadPoolExecutor?類給封裝了一下,這個(gè)線程池就是?Executors?工廠類,通過這個(gè)類來創(chuàng)建出的不同線程池對(duì)象,Executors?類在內(nèi)部把?ThreadPoolExecutor?創(chuàng)建好了,并且設(shè)置了不同的參數(shù),下面就使用?Executors?演示一下標(biāo)準(zhǔn)庫中線程池的效果吧。
? ? ? ? 如下圖所示,在?Executors?中內(nèi)置了很多版本的線程池,這里我們使用固定數(shù)目的線程池來簡(jiǎn)單演示一下線程池的效果即可。
? ? ? ? 下面使用線程池的具體代碼及運(yùn)行結(jié)果如下所示:?
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestDemo7 {public static void main(String[] args) {// ExecutorService 提供了一種管理和控制異步任務(wù)執(zhí)行的方式ExecutorService service = Executors.newFixedThreadPool(4);// 使用 submit 方法把任務(wù)添加到線程池中service.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}
? ? ? ? ?介紹完這兩個(gè)標(biāo)準(zhǔn)庫中的線程池,可以明確一點(diǎn),當(dāng)我們只是想簡(jiǎn)單用一下線程池,就可以使用?Executors ,當(dāng)我們希望高度定制化一個(gè)線程池,就可以使用?ThreadPoolExecutor。
三、實(shí)現(xiàn)線程池
? ? ? ? 在前面介紹了標(biāo)準(zhǔn)庫中的線程池及演示了使用的效果,下面我就來寫代碼實(shí)現(xiàn)一個(gè)簡(jiǎn)單的線程池,這里我就直接寫一個(gè)固定線程數(shù)目的線程池,下面是這個(gè)簡(jiǎn)單線程池中包含的內(nèi)容:
- 提供構(gòu)造方法,指定創(chuàng)建多少個(gè)線程;
- 在構(gòu)造方法中,把這些線程都創(chuàng)建好;
- 創(chuàng)建一個(gè)阻塞隊(duì)列,能夠持有要執(zhí)行的任務(wù);
- 提供?submit?方法,可以添加新的任務(wù)。
? ? ? ? 那么下面我就直接上代碼了,關(guān)于這個(gè)簡(jiǎn)單線程池實(shí)現(xiàn)的細(xì)節(jié)我會(huì)在代碼中以注釋的方式進(jìn)行介紹,線程池實(shí)現(xiàn)的代碼及運(yùn)行結(jié)果如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPoolExecutor {// 創(chuàng)建阻塞隊(duì)列,用來接收任務(wù),這里設(shè)置最多容納任務(wù)量為 100private BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(100);// 創(chuàng)建線程鏈表,把創(chuàng)建的每個(gè)線程都用線程鏈表組織起來private List<Thread> threadList = new ArrayList<>();// 構(gòu)造方法,指定線程池中固定的線程數(shù),并且將線程都創(chuàng)建好public MyThreadPoolExecutor(int num) {for (int i = 0; i < num; i++) {Thread t = new Thread(()->{while (true) {// 利用 runnable 來接收阻塞隊(duì)列中的任務(wù)Runnable runnable = null;try {// 獲取任務(wù)runnable = blockingQueue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}// 執(zhí)行任務(wù)runnable.run();}});// 啟動(dòng)線程t.start();// 將線程加入到線程鏈表中threadList.add(t);}}// 方法 sumbit 用來向阻塞隊(duì)列中添加新的任務(wù)public void sumbit(Runnable runnable) throws InterruptedException {blockingQueue.put(runnable);}
}public class ThreadDemo8 {public static void main(String[] args) throws InterruptedException {// 創(chuàng)建線程池,指定線程數(shù)目為 4MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);// 循環(huán) 100 次,向線程池中添加 100 個(gè)任務(wù)for (int i = 0; i < 100; i++) {int n = i;executor.sumbit(new Runnable() {@Overridepublic void run() {// 任務(wù)的內(nèi)容System.out.println("執(zhí)行任務(wù):->" + n + ",執(zhí)行的線程是:->" + Thread.currentThread().getName());}});}}
}
? ? ? ? 如上圖的運(yùn)行結(jié)果可以看出,多個(gè)線程之間的執(zhí)行順序是不確定的,某個(gè)線程獲取到了某個(gè)任務(wù),但并非是立即執(zhí)行,在這個(gè)過程中很有可能另一個(gè)線程就插到前面了,這里的這些線程彼此之間都是等價(jià)的。
·結(jié)尾
? ? ? ? 文章到這里就要結(jié)束了,回顧本篇文章,我介紹了什么是線程池,標(biāo)準(zhǔn)庫中線程池還有實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的線程池,其中還是要多理解一下標(biāo)準(zhǔn)庫中線程池構(gòu)造方法每個(gè)參數(shù)的意思,及理解拒絕策略的含義,這可以讓我們對(duì)?ThreadPoolExecutor?類的使用更加清晰,后面實(shí)現(xiàn)的線程池也就可以看出線程池基本的工作原理,那就是不斷利用這 4 個(gè)線程來執(zhí)行任務(wù),這樣就省去創(chuàng)建和銷毀線程的開銷,那么如果你感覺本篇文章對(duì)你有所幫助,還是希望能收到你的三連鼓勵(lì),如果對(duì)文章的內(nèi)容有所疑問歡迎在評(píng)論區(qū)進(jìn)行討論,我們下一篇文章再見吧~~~