1.什麼是線程池?
線程池是指在進程開始時創建一定數量(有上限)的線程,並放入池中,需要使用時,就從池中取出來一個可用線程,用完後,不是銷燬,而是返回池中。如果線程用完了,就需要等待有空閒的線程後,才能使用。
java在JDK1.5後就引入了線程池。所以不需要我們自己實現一個線程池。
舉例說明:
沒有使用線程池的時候,假設我們要看一本書“java編程思想”,是直接到網上買一本書,買來後,看完就丟棄(銷燬)。
使用多線程,這次我們不是直接到網上買一本書,都是通過到圖書館,借"java編程思想這本書",我看完後,歸回到圖書館,這時候其他的人,還可以繼續重複閱讀(線程的重複利用)。
2.實戰
2.1通過線程池代碼創建線程
public void two() throws Exception{
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int count=0;
for (int i = 0; i < 5; i++) {
Thread.sleep(1200);
count++;
}
return count;
}
};
ExecutorService e= Executors.newFixedThreadPool(10);
Future<Integer> f1=e.submit(callable);
Integer result = f1.get();
System.out.println("獲取多線程的值:"+result);
}
通過上述代碼,我們可以知道實現線程池涉及到ExecutorService和Executors。下面我們來一個個進行源碼分析
2.1.Executors源碼分析
在idea中,把光標放到Executors上,按住鼠標左鍵+ctrl進入Executors類。輸入alt+7查看該類下的所有方法。
- newFixedThreadPool(int nThreads) 創建一個重用固定數量線程的線程池,如果在所有線程都處於活動狀態時提交了額外的任務,他們將在隊列中等待,直到線程可用爲止。
- newWorkStealingPool(int parallelism) jdk1.8後引入的,它是新的線程池類ForkJoinPool的擴展,能夠合理的使用CPU,進行併發運行任務。可以瞭解爲,工作量一樣,A,B同時開發,誰開發的快,誰就多做一些,能者多勞(不提倡工作中這樣,只是爲了通過實例更好的理解這個概念)。
- newSingleThreadExecutor() 創建一個單例的線程池,也就是說池中就一個線程。通過這個線程來處理所有的任務,如果發現這個這個線程因爲失敗而關閉,不要慌,會有一個線程來取代他,保證任務能正常的運轉
- newCachedThreadPool() 創建一個可緩存線程池,如果沒有線程可用,發現60s內有線程不工作,會創建一個線程的線程來取代他,再放入池中。 可以理解爲一個公司爲了保證公司的正常運轉,會請20個人,但是,上班期間發現隔壁小王在偷懶,那不好意思,針對這種偷懶的人,公司表示不歡迎,直接開除,重新招一個人,保證公司的人員固定能正常運營。
- newSingleThreadScheduledExecutor() 創建一個單線程的線程池,此線程池的的線程可以定時週期性的運行任務。注意坑點:使用這種方法,如果出現異常,會導致無法正常的運行任務。所以,個人建議,使用這種方式的時候,run方法裏面的代碼可以加上異常處理邏輯。
- newScheduledThreadPool(int corePoolSize) 創建一個固定大小的線程池。此線程池支持定時以及週期性執行任務的需求。
newFixedThreadPool
創建一個重用固定數量線程的線程池,如果在所有線程都處於活動狀態時提交了額外的任務,他們將在隊列中等待,直到線程可用爲止。
package com.cxyxs.thread.four;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 11:12
* Modified By:
*/
public class NewFixedThreadPool {
public static void main(String[] args) {
//System.out.println(Runtime.getRuntime().availableProcessors());
Runnable run = new Runnable() {
@Override
public void run() {
try {
System.out.println("程序猿學社:"+new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executorService.execute(run);
}
}
}
爲了更好的看出效果,我特意把每個任務都延遲了3秒鐘,模擬真實的場景,各位社友,覺得他的輸出結果應該是怎麼樣的?
通過控制檯,我們可以發現,每次只處理兩個任務,而其他的任務處在排隊狀態,依次處理。
爲什麼只處理兩個任務?有沒有社友想到爲什麼?
這是因爲社長設置了線程池中的數據大小爲2。講到這裏有引發一個疑問。
線程池裏面的數據量可以隨便設置嗎?
不能隨便設置,不同的開發人員設置的標準不一樣,個人是CPU核數的2倍。
例如,你單個cpu,同一時間只有一個通道,可以運行任務。就算你設置100個線程,實際上也是交替運行的。
不要社長這樣說,就覺得CPU是單核,就覺得多線程沒有一點用,實際上,在網絡通訊過程中,解析包,處理包,就是通過多線程,把解析和處理的邏輯切割開來。實現解耦。這樣有一個好處,就算你處理業務邏輯的線程很慢,也不會影響我解析包的線程正常運轉。
newWorkStealingPool(int parallelism)
jdk1.8後引入的,它是新的線程池類ForkJoinPool的擴展,能夠合理的使用CPU,進行併發運行任務。
package com.cxyxs.thread.four;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 14:15
* Modified By:
*/
public class WorkStealingPool {
public static void main(String[] args) throws Exception{
//再測試之前,我們應該瞭解我們電腦的cpu核數,我的電腦是4核
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService executorService = Executors.newWorkStealingPool();
for (int i = 0; i < 10; i++) {
//產生一個隨機數1-3
int num = (int) (Math.random() * 3 + 1);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//模擬正在業務
Thread.sleep(num*1000);
System.out.println("線程名:"+Thread.currentThread().getName()+",業務時長:"+num+"秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executorService.execute(runnable);
}
//因爲是守護進程,如果不加這句話是無法看到結果的
System.in.read();
}
}
因爲newWorkStealingPool是併發運行,既然人工作的效率都有高低,電腦也是一樣。已經完成任務的,不可能讓他閒着,這樣會造成資源的浪費。
重點下面這句話要理解。打印出來的4,表示我們電腦只能並行4個線程。
Runtime.getRuntime().availableProcessors()
- 通過上面打印的線程名,我們可以發現他用的是ForkJoinPool線程池。這是jdk1.7引入的。
- 數字0-3 表示我們電腦同一時刻在工作(只是針對這塊而言,這句話擴大來說是有問題的)。
- 我們可以發現work1線程跑了3次,說明執行完,自己的任務後,發現其他的小夥子還沒有做完,開始繼續接任何開始做事了(rabbitMQ裏面有一個地方設置條數爲1,跟這裏的思想類似,意思就是我每次從隊列裏面取1條,我消費完,再繼續取)。
System.in.read();
- 因爲是守護進程,如果不加這句話是無法看到結果的,後文會詳細說明
newSingleThreadExecutor()
創建一個單例的線程池,也就是說池中就一個線程。通過這個線程來處理所有的任務,如果發現這個這個線程因爲失敗而關閉,不要慌,會有一個線程來取代他,保證任務能正常的運轉
package com.cxyxs.thread.four;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 14:48
* Modified By:
*/
public class NewSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index=i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(index == 5){ //故意搞破壞
int flag =index/0;
}
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
了驗證我上面那段話,我特意搞破壞,讓i=5的時候,除以0,大家都知道0不能作爲分母。
- 通過圖,我們可以發現,之前的線程名一直是XXX-1,到了第五次報錯後,就直接拋出異常,重新起了一個新的線程是XXX-2。
- newSingleThreadExecutor看名稱就知道是單例線程池的意思。那就只有一個線程。一次只能運行一個任務,所以他也是順序執行的。
newCachedThreadPool()
創建一個可緩存線程池,如果沒有線程可用,發現60s內有線程不工作,會創建一個線程的線程來取代他,再放入池中。 可以理解爲一個公司爲了保證公司的正常運轉,會請20個人,但是,上班期間發現隔壁小王在偷懶,那不好意思,針對這種偷懶的人,公司表示不歡迎,直接開除,重新招一個人,保證公司的人員固定能正常運營。
package com.cxyxs.thread.four;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 15:02
* Modified By:
*/
public class NewCachedThreadPool {
public static void main(String[] args) throws Exception{
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
int index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
if(index == 0){
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("第一次:"+Thread.currentThread().getName()+","+new Date());
}
});
}
Thread.sleep(7000);
for (int i = 0; i < 10; i++) {
int index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
if(index == 0){
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("第二次:"+Thread.currentThread().getName()+","+new Date());
}
});
}
}
}
首先我這個代碼看起來很亂。因爲自身爲了論證結論。所以沒有把代碼做一個簡單的抽取。
代碼設計:通過我的代碼,可以發現,第一次循環5次,我把第一條數據設置延遲80s,我還特意在main主線程裏面延遲了70s。,第二次,循環10次。爲什麼這樣設計?
- 就是爲了論證到了60秒以後,到底是不是把線程回收了。第一次第一個線程輸出後,本身線程數量爲9,被回收到了1。
- 線程的複用,我們可以看圖,XXXX-3,出現了2次,說明就是這個線程執行完後,如果這時候有線程過來,其他的線程就可以直接複用這個線程。
newSingleThreadScheduledExecutor()
創建一個單線程的線程池,此線程池的的線程可以定時週期性的運行任務。注意坑點:使用這種方法,如果出現異常,會導致無法正常的運行任務。所以,個人建議,使用這種方式的時候,run方法裏面的代碼可以加上異常處理邏輯。這種方式newSingleThreadExecutor類似,只是增加了週期性運行,這裏不過多的闡述。
newScheduledThreadPool(int corePoolSize)
創建一個固定大小的線程池。此線程池支持定時以及週期性執行任務的需求。
package com.cxyxs.thread.four;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 15:43
* Modified By:
*/
public class NewScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("開始時間:"+Thread.currentThread().getName()+","+new Date());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},2000,6000,TimeUnit.MILLISECONDS);
}
}
通過截圖我們可以發現是6秒一次。
- 保證period>initialDelay,以period爲準。以period的時長爲一個週期
- 如果run方法運行時間大於period,定時任務的週期以run運行時長爲一個週期。
- 詳細這個方法的時候,本文就不闡述了,後續會出相關文章。