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源码

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