【关于java线程池】聊一聊ThreadPoolExecutor

最近在看java线程池的实现,主要是ThreadPoolExecutor,因此写个博客记录下来。

假设我们创建了一个线程池,其中核心线程数为3,最大线程数为6,任务队列的长度为5。

ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 6, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(5));

那么根据ThreadPoolExecutor的doc描述来看,我们可以得到以下的信息

核心线程数和最大线程数

  1. 当我们往pool中添加任务,也就是执行execute(Runnable)方法时
  • 如果此时pool中的线程数少于3,一个新的线程就会被创建,即使当前pool中存在空闲的线程
  • 如果此时pool中的线程数大于3,小于6,那么当前仅当任务队列满的时候,才会有新的线程被创建
  1. 线程池的核心线程数以及最大线程数在运行过程中,也可以动态设定,通过执行
 pool.setCorePoolSize(10);
 pool.setMaximumPoolSize(10);
  • 我们这里设定了一个固定大小的线程池,也就是 fixed-size thread pool

新线程的创建

  1. 在线程池构造好之后,线程并不会被马上创建好,只有当有任务提交到线程池中,新线程才会被创建然后开始运行,不过我们可以调用prestartCoreThreadprestartAllCoreThreads方法,提前创建出新线程。

  2. 线程的创建会由线程工厂来负责,线程工厂可以是由我们指定,也可以使用默认的线程工厂,以默认的线程工厂为例,其创建的所有线程均在同一个线程组中,并且拥有相同的优先级(NORM_PRIORITY),同时均是非守护线程(这也导致线程池不会自己停掉)。

超时时间

  1. 当前池中的线程数为5,并且有3个线程的空闲时间超过了keepAliveTime,那么会有2个线程被停止
  2. 我们也可以在程序执行中动态的设定超时时间
pool.setKeepAliveTime(30, TimeUnit.MINUTES);
  1. 我们也可以用同样的策略停止核心线程数的线程
pool.allowCoreThreadTimeOut(true);

任务入队

任务入队的方式和当前线程池的大小有关系,下面具体情况说明

  1. 假如当前有2个线程在运行,通常线程池会新建一个线程,而不是让提交的任务入队
  2. 假如当前有3个线程在运行,通常线程池会让提交的任务入队,而不是新建一个线程
  3. 如果当前队列已满,并且当前线程池没有超过最大线程数,此时会创建一个新的线程;如果当前线程数达到了最大线程数,此时会触发拒绝策略。

勾子方法

重写以下的方法,在执行任务前和执行任务后,做一些定制的操作。

beforeExecute(Thread, Runnable)
afterExecute(Runnable, Throwable)
我们在这里定制一个可以暂停和重新恢复的线程池
public class PausableThreadPoolExecutor extends ThreadPoolExecutor {

    private boolean isPaused;
    private ReentrantLock pauseLock = new ReentrantLock();
    private Condition unpaused = pauseLock.newCondition();

    public PausableThreadPoolExecutor(···) {
        super(···);
    }
    
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        pauseLock.lock();
        try {
            while (isPaused)
                unpaused.await();
        }catch (InterruptedException ie){
            t.interrupt();
        }finally {
            pauseLock.unlock();
        }
    }
    
    public void pause(){
        pauseLock.unlock();
        try {
            isPaused = true;
        }finally {
            pauseLock.unlock();
        }
    }
    
    public void resume(){
        pauseLock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        }finally {
            pauseLock.unlock();
        }
    }
}

线程池的状态

private static final int RUNNING = -1 << COUNT_BITS;
此状态下,可以接受新的任务并且执行在队列里的任务

private static final int SHUTDOWN = 0 << COUNT_BITS;
此状态下,可以不接受新的任务,但是可以执行在队列里的任务

private static final int STOP = 1 << COUNT_BITS;
此状态下,不接受新的任务,也不处理在队列里的任务,并且会中断在执行过程中的任务

private static final int TIDYING = 2 << COUNT_BITS;
此状态下,所有的任务都已经终结,工作线程数为0,并且会调用terminated()勾子方法

private static final int TERMINATED = 3 << COUNT_BITS;
此状态下,terminated()方法已经完成

内部类Worker的实现

类Worker主要是为了维护运行任务的线程的中断控制状态,以及一些其他的次要的记录。

final Thread thread; 记录这个worker运行的线程
Runnable firstTask; 初始化运行的任务
volatile long completedTasks; 完成的任务数

Worker的构造方法需要传递一个Runnable firstTask来初始化

当worker执行run方法时,会将程序的逻辑委托给外部的runWorker方法,那么runWorker的运行流程是什么样的呢?

  1. 主逻辑是一个循环,从队列中获取task,如果task != null就执行循环里的逻辑
  • 这里介绍下从队列中获取task的方法——getTask()
    这个方法是从任务队列中获取任务,在等待任务的过程中,可能会超时,可能会不停地阻塞,这个取决去线程池的配置。当然也可能由于worker必须得退出,导致获取的任务为空,在以下几种情况时,woker必须得退出。(1)池里面有超过最大线程数的worker存在,在程序运行过程中,调用setMaximumPoolSize方法时,就极有可能出现此种情况;(2)当前池处于stopped状态;(3)当前池处于shut down状态,并且任务队列为空;(4)当前worker获取任务超时,这是由于我们设置了allowCoreThreadTimeOut或者当前worker数大于核心线程数。
  1. 获取到非null的task后,会依次执行beforeExecute(wt, task);task.run();afterExecute(task, thrown);

execute(Runnable command)方法

分为三部曲

  1. 如果少于核心线程数的线程在运行,那么就新建一个worker,并把command当作第一个task
  2. 如果线程池在运行,并且任务队列能接收command,那我们就将command添加进任务队列,接着进行判断,是否需要新添加worker或者拒绝当前的command
  3. 直接添加worker,同样将command当作第一个task,如果失败,就拒绝当前的command

拒绝策略

CallerRunsPolicy

task的执行由调用者执行,也就是由执行execute(Runnable command)的线程执行,前提是线程池没有shut down,否则,该任务会被丢弃。

AbortPolicy

直接抛出异常

DiscardPolicy

直接丢弃任务,不做出任何的提示

DiscardOldestPolicy

丢弃任务队列中的第一个任务(也就是加入队列时间最久的任务),然后重新执行execute(command)方法

猜你喜欢

转载自blog.csdn.net/weixin_38014667/article/details/107870423