5、深入理解java線程池實現原理、合理配置

一、什麼是線程池、爲什麼要使用線程池

  • 概念
    存放一組線程的容器就是線程池
  • 作用
    可以降低資源的消耗。降低線程創建和銷燬的資源
    提高響應速度,可以省去線程創建和銷燬的時間
    提高線程的可管理性

二、實現一個自己的線程池

線程池所需要的組件
1、保存線程的容器
2、保存任務的隊列(使用阻塞隊列即可)
3、線程能夠接受外部的任務
4、線程池能運行外部的任務

package cn.enjoy.controller.thread.ThreadPoll;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @author:wangle
 * @description:線程池實現
 * @version:V1.0
 * @date:2020-04-05 21:56
 **/
public class MyThreadPoll {

    //默認的線程個數
    private static final int WORK_THREAD_NUM = 5;

    //任務隊列的大小
    private static final int QUEUE_NUM = 100;

    private final BlockingQueue<Runnable> taskQueue;

    private final int workNum;

    private final WorkThread[] workThreads;

    public MyThreadPoll(int workNum,int taskNum){
        if(workNum<=0)
            workNum = WORK_THREAD_NUM;
        this.workNum=workNum;
        if (taskNum<=0)
            taskNum=QUEUE_NUM;
        taskQueue = new ArrayBlockingQueue<>(taskNum);
        workThreads = new WorkThread[workNum];
        for(int i=0;i<workNum;i++){
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }

    }

    //內部類,從阻塞隊列取數據進行執行
    private  class WorkThread extends Thread{

        @Override
        public void run() {
            Runnable r = null;
            try{
                while(!isInterrupted()){
                    r = taskQueue.take();
                    if(null != r){
                        System.out.println("準備執行任務"+r);
                        r.run();
                    }
                    r=null;
                }
            }catch (InterruptedException e){

            }
        }
        public void stopWorker(){
            isInterrupted();
        }
    }
    
    public void execute(Runnable task ){
        taskQueue.offer(task);
    }
}
package cn.enjoy.controller.thread.ThreadPoll;

/**
 * @author:wangle
 * @description:
 * @version:V1.0
 * @date:2020-04-05 22:21
 **/
public class test {

    public static class MyTask implements Runnable{

        private String name;

        public MyTask(String name){
            this.name=name;
        }

        public String getName(){
            return name;
        }

        @Override
        public void run() {
            try{
                Thread.sleep(500);
                System.out.println(Thread.currentThread().getName()+"執行完成");
            }catch (InterruptedException e){
                //TODO DEAL
            }

        }
    }
    public static void main(String[] args)throws InterruptedException{
        MyThreadPoll   myThreadPoll  = new MyThreadPoll(3,0);
        myThreadPoll.execute(new MyTask("1"));
        myThreadPoll.execute(new MyTask("2"));
        myThreadPoll.execute(new MyTask("3"));
        myThreadPoll.execute(new MyTask("4"));
        myThreadPoll.execute(new MyTask("5"));
        Thread.sleep(100000);
    }
}

在這裏插入圖片描述
線程池容量是3,我們啓動了5個任務,可以明顯看到,前3個任務處理完成之後,後2個任務纔開始被執行。

當前線程池有沒有什麼缺點

  • 無法調整線程的數量,可能會造成資源的浪費
  • 如果線程都被任務佔用了,並且阻塞隊列也滿了,那麼外部線程調用add或者offer方法的時候,這會使得外部線程阻塞,影響調用者性能。

JDK線程池很好的解決了當前這2個問題,下面研究一下。

三、JDK中線程池剖析

初始化

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

參數解析
int corePoolSize
核心線程數量,
int maximumPoolSize
最大線程數量
long keepAliveTime
線程存活時間,這個是對於大於核心線程數量的線程纔有效的
TimeUnit unit
時間單位
BlockingQueue<Runnable> workQueue
保存任務的阻塞隊列
ThreadFactory threadFactory
創建線程的工廠,給線程賦予名稱
RejectedExecutionHandler handler
飽和策略:當線程已經達到最大線程數並且阻塞隊列已經滿了,要進行的操作,分爲以下四種。也可以重寫當前接口,自定義邏輯
AbortPolicy :直接拋出異常,默認;
CallerRunsPolicy:用調用者所在的線程來執行任務
DiscardOldestPolicy:丟棄阻塞隊列裏最老的任務,隊列裏最靠前的任務
DiscardPolicy :當前任務直接丟棄

  • 添加任務
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        int c = ctl.get();
        //如果線程數還未到達核心線程數,增加線程並且執行任務
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果核心線程都已經被佔用,向隊列中添加任務
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果核心線程都被佔用,並且隊列已滿,and線程數小於最大線程數,則新增線程,進行執行任務。如果線程已經達到了最大線程數則執行我們初始化是傳進來的飽和策略函數
        else if (!addWorker(command, false))
            reject(command);
    }

java線程池的執行圖解在這裏插入圖片描述
1、添加任務,如果線程數還未到核心線程數,直接添加線程並且執行任務
2、如果線程數已經到達了核心線程數,就將任務添加到阻塞隊列當中
3、如果阻塞隊列也滿了,並且當前線程數還未達到最大線程數,就新增線程執行任務
4、如果當前線程數也達到了最大線程,就執行我們的飽和策略。

提交任務

execute(Runnable command) 不需要返回
Future submit(Callable task) 需要返回

關閉線程池

shutdown(),shutdownNow();
shutdownNow():設置線程池的狀態,還會嘗試停止正在運行或者暫停任務的線程
shutdown()設置線程池的狀態,只會中斷所有沒有執行任務的線程

三、合理配置線程池JDK

根據任務的性質來:計算密集型(CPU),IO密集型,混合型

  • 計算密集型(CPU)例如:加密、大數分解正則、以及需要在內存中進行處理的操作。線程數適當小一點,建議爲cpu核數+1,因爲線程數較多可能會導致大部分時間都消耗在線程的上下文切換上,+1是因爲防止頁缺失,當發生缺頁中斷的時候線程會被掛起等待,這時候cpu就會空閒出一個核。
  • IO密集型:讀取文件、數據庫連接操作、網絡通訊等,線程數稍微大一點,因爲I/O一般操作都是比較慢的,連接過小容易導致線程都阻塞等待返回,處理效率不高,一般建議設置爲cpu核數*2
  • 混合型,儘量拆分,如果I/O~=計算密集就可以拆分出來,性能提升比較明顯

四、預定義的線程池

JDK爲我們提供了幾種預定義的線程池下面分析下

  • FixedThreadPool
    創建固定線程數量的、適用於負載較重的服務器,使用無界阻塞隊列

  • SingleThreadExecutor
    創建單個線程、需要保證任務順序性執行,使用無界阻塞隊列

  • CachedThreadPool
    會根據需要來創建新線程的,執行很多短期異步任務的程序,使用了SynchronousQueue(隊列不存儲任務,只做轉發)

  • WorkStealingPool
    基於ForkJoinPool實現工作密取的線池子

  • ScheduledThreadPoolExecutor
    定期執行週期任務的線程池、類似於定時任務
    1、newSingleThreadScheduledExecutor
    只包含一個線程、只需要單個線程執行週期任務,保證順序的執行各個任務
    2、newScheduledThreadPool :可以包含多個線程的,線程執行週期任務,適度控制後臺線程數量的時候
    方法辨析
    schedule:只執行一次,任務還可以延時執行
    scheduleAtFixedRate:提交固定時間間隔的任務,
    scheduleWithFixedDelay:提交固定延時間隔執行的任務,就是第一個任務的結束到第二個任務的開始的時間

建議在提交給ScheduledThreadPoolExecutor的任務要住catch異常。否則任務出現異常後任務就會掛掉,不會再次週期性執行

五、Executor框架

在這裏插入圖片描述
線程池可接收Runnable和Callable的方法
execute提交沒有返回值。submit提交有返回值
返回值需要用Future的get方法去獲取到
還可以通過cancel來中斷任務

五、CompletionService

如果我們的任務需要有返回值,在把任務加到線程池中後,怎麼樣去獲取任務結果,很明顯,我們得首先把任務保存起來,然後再去get結果,這樣會產生一個問題:我們get到結果的順序肯定是任務加入到線程池中的順序,理想狀態是什麼,當然是那個線程先結束我們就先get到那個結果,這樣性能無疑是最好的。 CompletionService就是做這事的

package cn.enjoy.controller.thread.ThreadPoll;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author wangle25
 * @description completionService
 * @date 09:51 2020-04-08
 * @param 
 * @return 
 **/

public class CompletionCase {
	//線程數爲當前機器cpu核數,我的是4核,如果是8核那麼就是8個Thread
    private final int POOL_SIZE = Runtime.getRuntime().availableProcessors();
    private final int TOTAL_TASK = Runtime.getRuntime().availableProcessors();

    // 方法一,自己寫集合來實現獲取線程池中任務的返回結果
    public void testByQueue() throws Exception {
    	long start = System.currentTimeMillis();
    	//統計所有任務休眠的總時長
    	AtomicInteger count = new AtomicInteger(0);
        // 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        //容器存放提交給線程池的任務
        BlockingQueue<Future<Integer>> queue = 
        		new LinkedBlockingQueue<Future<Integer>>();

        // 向線程池增加task
        for (int i = 0; i < TOTAL_TASK; i++) {
            Future<Integer> future = pool.submit(new WorkTask("ExecTask" + i));
            queue.add(future);//i=0 先進隊列,i=1的任務跟着進
        }

        // 檢查線程池任務執行結果
        for (int i = 0; i < TOTAL_TASK; i++) {
            //這裏肯定是先入隊列的先取到,然後再調用get方法,但是如果線程還沒完成,則會阻塞再這裏一直等待線程完成任務,才能繼續取後邊的任務,儘管後邊的任務已經有可能已經done了
        	int sleptTime = queue.take().get();
        	System.out.println(" slept "+sleptTime+" ms ...");        	
        	count.addAndGet(sleptTime);
        }

        // 關閉線程池
        pool.shutdown();
        System.out.println("-------------tasks sleep time "+count.get()
        		+"ms,and spend time "
        		+(System.currentTimeMillis()-start)+" ms");
    }

    // 方法二,通過CompletionService來實現獲取線程池中任務的返回結果
    public void testByCompletion() throws Exception {
    	long start = System.currentTimeMillis();
    	AtomicInteger count = new AtomicInteger(0);
        // 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        CompletionService<Integer> cService = new ExecutorCompletionService<>(pool);
        
        // 向裏面扔任務
        for (int i = 0; i < TOTAL_TASK; i++) {
        	cService.submit(new WorkTask("ExecTask" + i));
        }
        
        // 檢查線程池任務執行結果
        for (int i = 0; i < TOTAL_TASK; i++) {
        	int sleptTime = cService.take().get();
        	System.out.println(" slept "+sleptTime+" ms ...");        	
        	count.addAndGet(sleptTime);
        }        

        // 關閉線程池
        pool.shutdown();
        System.out.println("-------------tasks sleep time "+count.get()
			+"ms,and spend time "
			+(System.currentTimeMillis()-start)+" ms");
    }

    public static void main(String[] args) throws Exception {
        CompletionCase t = new CompletionCase();
        t.testByQueue();
        t.testByCompletion();
    }
}

在這裏插入圖片描述
結果辨析
1、我們自己實現的獲取結果的方法,很明顯最後一個Thread只執行了371ms但是它確實最後被獲取到的,因爲第一個線程最先被獲取到然後get,然後就一直阻塞到第一個線程完成,纔會去獲取其他剩餘線程。總共耗時899ms
2、使用completionService我們可以看到線程被獲取到值的順序就是線程執行完成的先後順序,總共耗時623ms

綜上,如果線程數量更大的話,性能提升會更加明顯。

實現:completionService我沒看源碼,但是它底層是又用了另一個阻塞隊列,我理解應該是那個線程先結束,就將它添加到阻塞隊列,然後再get值,就可以保證線程的結束的先後順序。有興趣童鞋可以去看看jdk源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章