java(jdk1.8)五種線程池,你都知道具體的應用場景嗎?

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運行時長爲一個週期。
  • 詳細這個方法的時候,本文就不闡述了,後續會出相關文章。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章