xiaobenchi

导航

Java并发包中线程池ThreadPollExecutor原理探究

Java并发包中线程池ThreadPollExecutor原理探究

1. 介绍

线程池主要解决两个问题:一是当执行大量异步任务时线程池能够提供较好的性能。在二是线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。

ThreadPoolExecutor继承了AbstractExecutorService,成员变量ctl是一个Integer的原子变量,用来记录线程池状态和线程池中线程个数,类似于ReentrantReadWriteLock使用一个变量来保存两种信息。

线程池状态含义

  • RUNNING: 接收新任务并且处理阻塞队列里的任务。
  • SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务。
  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。
  • TIDYING:所有任务都执行完(包含阻塞队列里面的任务)。
  • TERMINATED:终止状态。terminated方法调用完成以后的状态。

线程池参数

  • corePoolSize: 核心线程个数。
  • workQueue:用于保存等待执行的任务的队列。
  • maximunPoolSize:线程池最大线程的数量
  • ThreadFactory:创建线程的工厂
  • RejectedExecutionHandler:饱和策略
  • keeyAliveTime:存活时间
  • TimeUnit:存活时间的单位

线程池类型

  • newFixedThreadPool :创建一个核心线程个数和最大线程个数都为nThreads的线程池。并且阻塞队列长度为Integer.MAX_VALUE。
  • newSingleThreadExcutor:创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VAlUE。
  • newCacheThreadPool:创建一个按需创建线程的线程池,初始线程个数为0,最多线程个数为Integer.MAX_VALUE。

2. 源码分析

  • public void execute(Runnable command)

    execute方法的作用是提交任务command到线程池进行执行。

    image-20220812111214568

    ThreadPoolExecutor的实现实际是一个生产消费模型,当用户添加任务到线程池时相当于生产者生产元素,workers线程工作集中的线程直接执行任务或者从任务队列里面获取任务时则相当于消费者消费元素。

    public void execute(Runnable command){
        
        //(1)如果任务为null,则抛出NPE异常
        if(command == null)
            throw new NullPointerException();
        
        //(2)获取当前线程池的状态+线程个数变量的组合值
        int c = ctl.get();
        
        //(3)当前线程池中线程个数是否小于corePoolSize,小于则开启新线程运行
        if(workerCountOf(c) < corePoolSize){
            if(addWorker(command,true))
                return;
            c = ctl.get();
        }
        
        //(4)如果线程池处于RUNNING状态,则添加任务阻塞队列
      if(isRunning(c) && workQueue.offer(command)){
            
            //(4.1)二次检查
            int recheck = ctl.get();
            
            //(4.2)如果当前线程池状态不是RUNNING则从队列中删除任务,并执行拒绝策略
            if(!isRunning(recheck) && remove(command))
                reject(command);
            //(4.3)否则如果当前线程池为空则添加一个线程
            else if(workerCountOf(recheck) == 0)
                addWorked(null,false);
        }
        
        //(5)如果队列满,则新增线程,新增失败则执行拒绝策略
        else if(!addWorker(command,false))
            reject(command);
    }
    
    • addWorker分析

      代码主要分为两个部分:第一部分双重循环通过CAS操作增加线程数;第二部分主要是把并发安全的任务添加到workers里面,并启动任务执行。

  • 工作线程Worker的执行

    用户线程提交任务到线程池后,由Worker来执行。

    Worker(Runnable firstTask){
        setState(-1); //在调用runWorker前禁止中断
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this); //创建一个线程
    }
    

    在构造函数内首先设置Worker的状态为-1,这是为了避免当前Worker在调用runWorker方法前被中断。

    final void runWorker(Worker w){
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); //(9)将state设置为0,允许中断
        boolean completedAbruptly = true;
        try{
            //(10)
            while(task != null || (task = getTask()) != null){
                //(10.1)
                w.lock();
                ...
                try{
                    //(10.2)执行任务前做一些事情
                    beforeExecute(wt,task);
                    Throwable thrown = null;
                    try{
                        task.run(); //(10.3)执行任务
                    }catch(RuntimeException x){
                        thrown = x; throw x;
                    }catch(Error x){
                        thrown = x; throw x;
                    }catch(Throwable x){
                        thrown = x; throw new Error(x);
                    }finally{
                        //(10.4)执行任务完毕后做一些事情
                        afterExecute(task,thrown);
                    }
                }finally{
                    task = null;
                    //(10.5)统计当前Worker完成了多少个任务
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        }fianlly{
            //(11)执行清理工作
            processWorkerExit(w,completedAbruptly);
        }
    }
    
  • shutdown操作

    调用shutdown方法后,线程池就不会再接受新的任务了,但是工作队列里面的任务还是要执行的。该方法会立刻返回,并不等待队列任务完成再返回。

    public void shutdown(){
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try{
            //(12)权限检查
            checkShutdownAccess();
            
            //(13)设置当前线程池状态为SHUTDOWN,如果已经SHUTDOWN则直接返回
            advanceRunState(SHUTDOWN);
            
            //(14)设置中断标志
            interruptIdleWorkers();
            onShutdown();
        }finally{
    		mainLock.unlock();
        }
        //(15)尝试将状态变为TERMINATED
        tryTerminate();
    }
    

    如上代码中,代码12检查看是否设置了安全管理器,是则看当前调用shutdown命令的线程是否有关闭线程的权限,如果有权限则还要看调用线程是否有中断工作线程的权限,如果没有则抛出SecurityException或者NullPointerException异常。

    代码13,如果当前线程池状态>=SHUTDOWN则直接返回,否则设置为SHUTDOWN状态。

    代码14,设置所有空闲线程的中断标志。这里首先加了全局锁,同时只有一个线程可以调用shutdown方法设置中断标志。然后尝试获取worker自己的锁,获取成功则设置中断标志。由于正在执行的任务已经获取了锁,所以正在执行的任务没有被中断。这里中断的是阻塞到getTask()方法并企图从队列里面获取任务的线程,也就是空闲线程。

  • shutdownNow操作

    调用shutdownNow方法后,线程池就不会再接受新的任务了,并且会丢弃工作队列里面的任务,正在执行的任务会被中断,该方法会立刻返回,并不等待激活的任务执行完成。返回值为这时候队列里面被丢弃的任务列表。

    public List<Runnable> shutdownNow(){
        
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try{
            checkShutdownAccess(); //(16)权限检查
            advanceRunState(STOP); //(17)设置线程池状态为STOP
            interruptWorks(); //(18)中断所有线程
            tasks = drainQueue(); //(19)将队列任务移动到tasks中
        }finally{
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
    
  • awaitTermination操作

    当线程调用awaitTermiantion方法后,当前线程会被阻塞,直到线程池状态变为TERMIANTED才返回,或者等待时间超时才返回。

3. 总结

线程池巧妙地使用一个Integer类型的原子变量来记录线程池状态和线程池中的线程个数。通过线程池状态来控制任务的执行,每个Worker线程可以处理多个任务。线程池通过线程的复用减少了线程创建和销毁的开销。

posted on 2022-08-13 11:26  小迟在努力  阅读(72)  评论(0)    收藏  举报