初探ThreadPoolExcutors

363 阅读11分钟

简介

在项目中,会通过创建线程池来实现一些具体的数据拉取任务,相比于for循环方式的顺序拉取,能够有效保证中途拉取数据出现异常时,不会影响到后续的绝大多数数据。使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗内存或者“过度切换”的问题。

线程池的优势

  • 减少了重复建立和销毁线程的开销;
  • 避免因为任务过多创建太多线程,方便管理,线程可以统一销毁;
  • 提高系统的可扩展性,利用线程池实现一些异步任务,减少消耗时间,相比在一个线程里面,提高成功率,例如拉取数据;

创建一个线程池

在项目中的TaskBatchExecutor类(其继承了Thread类)里面有如下的一个私有变量,表示为在当前线程下的一个线程池对象。

private ThreadPoolExecutor executorService;

其专门为这一个TaskBatchExecutor负责,而一个TaskBatchExecutor会为一个单独的TaskBatch负责。因此,TaskBatchExecutor中重写的run方法内,其主要任务就是检测是否还有任务没有完成,并不断往线程池里面推送任务。

如下,为TaskBatchExecutor的构造函数:

    public TaskBatchExecutor(TaskService taskService, TaskBatch batch) {
        this.taskService = taskService;
        this.batch = batch;
        int poolNum = poolNumber.getAndIncrement();
        executorService = (ThreadPoolExecutor) Executors
            //在这里,调用了newFixedThreadPool来创建一个新的线程池
                .newFixedThreadPool(batch.getWorkerCount(), new ThreadFactory() {
                    AtomicInteger threadNumer = new AtomicInteger(1);

                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r,
                                String.format("batchTask-pool%d-t%d", poolNum,
                                        threadNumer.getAndIncrement()));
                    }
                });
    }

newFixedThreadPool()

可以看到,newFixedThreadPool实际上调用的是ThreadPoolExecutor方法。

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

通过更改ThreadPoolExecutor方法里的参数,我们可以实现自定义线程池。

ThreadPoolExecutor()

上一部分调用了ThreadPoolExecutor的构造函数之一,如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

其一共有几个参数:

  • corePoolSize核心线程数量,这些核心线程一旦创建,在没有任务的时候,线程也会保证一直存在
  • maximumPoolSize允许在线程池内同时存在的线程最大数量,即非核心线程+核心线程
  • keepAliveTime这是非核心线程在空闲时候的存活时间
  • unit是存活时间参数的单位
  • workQueue是任务队列,用来递交过来但还没来得及执行的任务
  • threadFactory线程工厂

可以看到在方法内又重新调用了另一个构造函数,并且加入了一个参数defaultHandler

  • defaultHandler参数用来决定任务由于线程限制或者任务队列容量不够时的处理方式
    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;
    }

常用的工作队列类型

SynchronousQueue:同步移交队列

适用于非常大的无界的,并且可以拒绝任务的线程池,这个队列如同名字一样,不会存储工作内容,而是直接移交给线程池去工作,如果没有空闲线程就会创建新的线程;

LinkedBlockingQueue:基于链表结构的阻塞队列

这是一个永远不会装满的工作队列,因此,一旦线程池内的核心线程都在工作,就不会创建新的非核心线程;

ArrayBlockingQueue:基于数组结构的有界阻塞队列

根据工作队列的上界,我们会依次进行创建核心线程,进入队列等待,创建非核心线程。

DelayQueue:延迟队列

队列内的元素必须实现 Delayed 接口。当任务提交时,入队列后只有达到指定的延时时间,才会执行任务

PriorityBlockingQueue:优先级阻塞队列

根据优先级执行任务,优先级是通过自然排序或者是Comparator定义实现。

四种预设定的线程池

newCachedThreadPool():可缓存的线程池

这个线程池全部都是非核心线程,线程最大数量为Integer.MAXVALUE,因此,工作队列可以采用同步移交队列,一旦有任务就交到线程池去执行;

newFixedThreadPool():定长线程池

定长的线程池,根据传参,控制核心工作线程的数量,且只有核心线程,工作队列可以采用链表阻塞队列,可以无限往里面添加任务;

newScheduledThreadPool():定时线程池

会传入一个核心线程数量,非核心数量为Integer.MAXVALUE,工作队列采用延时队列;

newSingleThreadExecutor():单线程化的线程池

只创建一个工作线程执行任务,若这个唯一的线程异常故障了,会新建另一个线程来替代,newSingleThreadExecutor可以保证任务依照在工作队列的排队顺序来串行执行。

四种handler

AbortPolicy

这是一个默认handler,其在出现任务被拒绝的时候,抛出异常

    public static class AbortPolicy implements RejectedExecutionHandler {
        public AbortPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

CallerRunsPolicy

这个策略的意思是,把任务回退给调用这个execute方法往线程池里面注入任务的线程;

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

DiscardPolicy

对于任何被拒绝的任务,直接丢弃。

    public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

DiscardOldestPolicy

当有任务被拒绝的时候,丢弃等待队列中最老的任务。

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

线程池执行

execute()

结合源码注释,可以很清楚的明白execute的功能:

  • 将command任务交付给线程池
  • 任务会被一个新的线程或者已经在线程池中存在的空闲线程执行
  • 如果说这个任务无法被执行,则会按照事先定义好的handler处理

一个任务递交到线程池的流程:

  1. 当一个任务被递交到一个全新的线程池,首先会创建核心线程来执行任务,直到核心线程到达数量限制。
  2. 当核心线程数到限制数量后,继续进来的任务就会被放到workQueue中,等待执行。
  3. 如果workQueue被放满了,就尝试创建新的非核心线程来执行任务。
	public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        //ctl是一个AtomicInteger,c可以表征这个线程池的状态,也可以通过一系列运算获得workCount数量等信息
        int c = ctl.get();
        //通过workerCountOf()方法,获得了核心线程数,如果发现小于我们设定的核心线程数量,则增加一个worker
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果在线程数大于核心线程数,先判断线程池是不是RNNING状态,其次,看是否可以往工作队列插入一个task
        if (isRunning(c) && workQueue.offer(command)) {
            //如果可以,我们还需要进行二次确认,避免线程池其余线程的任务变化导致的线程池变化
            int recheck = ctl.get();
            //如果线程池不在是RUNNING状态并且我们可以从WorkQueue中移除task,就直接reject
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //否则说明线程池可能是RUNNING状态,并且无法移除task,就判断线程数是否为0,若是的话则创建一个非核心线程让他去拿WorkQueue中的任务
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果线程池不是RUNNING状态或者无法插入task,就尝试直接用一个非核心线程直接执行该task
        else if (!addWorker(command, false))
            reject(command);
    }

ctl

ctl的主要作用是记录线程池的生命周期状态和当前工作的线程数。作者通过巧妙的设计,将一个整型变量按二进制位分成两部分,分别表示两个信息。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
	//两个工具常量,其中Integer.SIZE为32
    private static final int COUNT_BITS = Integer.SIZE - 3; //实际值为29
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
	//CAPACITY的值为1左移了29位并减1,从二进制的角度来看,其可以理解为:
	//0000 0000 0000 0001
	//1 << 29 - 1
	//0001 1111 1111 1111 即32,31,30的高位为0,其余都为1

	//在接下来的代码中,COUNT_BITS用来分隔runState和workCount的位数;而CAPACITY作为取这两个变量的工具。

    //线程池的状态有5个,所以COUNT_BITS需要减3
    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;
    private static final int TERMINATED =  3 << COUNT_BITS;

    //这样的话就非常好理解了,整个二进制数字的高位三位是runState,其通过与CAPACITY的反码做与计算得来
	//与之对应的,后29位就是wokerCount的数量了
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池状态切换

RUNNING:

能够接收新任务,以及对已添加的任务进行处理。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。

SHUTDOWN:

线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

STOP:

线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

TIDYING:

当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

TERMINATED:

线程池彻底终止,就变成TERMINATED状态。

这里可以顺带复习一下线程的生命周期

  1. 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  2. 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  3. 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  4. 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  5. 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

addWorker()

当增加一个worker成功的时候返回true,参数中的firstTask是在execute()中传入的Runnable command参数。

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 一个最基本的判断条件就是,如果rs是RUNNING状态即rs >= SHUTDOWN(false)时,会直接跳过这个if。其次,若rs >= SHUTDOWN(true),则需要保证rs == SHUTDOWN,firstTask == null,!workQueue.isEmpty()三个条件均满足,否则就return false;
            // 
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    //如果当前worker线程的数量大于等于核心线程数量(core=true),或者最大数量(core=false),返回true
                    return false;
                	//采用CAS操作让workerCount增加1
                if (compareAndIncrementWorkerCount(c))
                    //如果返回了true,说明增加成功了,直接跳出循环
                    break retry;
                
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    //如果状态变化了,调到外循环重新判断当前runState
                    continue retry;
                //否则就是CAS操作失败了,重新进行内循环即可
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //构建一个Worker对象
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //workers是一个HashSet,存储了这个线程池内的所有worker
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            //更新线程池的size
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //启动和Worker绑定的线程,调用了Worker的run方法,进而调用了runWorker(this),进而调用了我们task的run方法(其中就是我们真正要执行的任务);
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //如果线程启动失败
                addWorkerFailed(w);
        }
        return workerStarted;
    }
------------------------------------------------------------------------------------------------------------
		//Worker对象的构造函数,其实就是把task绑定并且用ThreadFactory创建了一个新的线程,并且把自己绑到了这个线程上,为啥呢么可以这么做?因为Worker也实现了Runnable接口并重写了run方法
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        public void run() {
            //在这个方法中,会执行task的run()方法
            runWorker(this);
        }

线程池中的Worker类

私有属性

Worker类存在几个私有属性,分别为:

/** Thread this worker is running in.  Null if factory fails. */
final Thread thread;
/** Initial task to run.  Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;

Worker的创建

创建一个Worker的时候需要给他绑定一个任务和执行这个work的线程对象:

Worker(Runnable firstTask) {
  setState(-1); // inhibit interrupts until runWorker
  this.firstTask = firstTask;
  this.thread = getThreadFactory().newThread(this);
}

*核心线程是如何保证没有任务也不会销毁的?

ThreadPoolExcutor中存在一个runWorker()方法:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

在其中的while循环中,会不断尝试获取task,其中重点在于这个getTask()方法,这个方法一旦返回了一个null,这个线程就会被回收

    private Runnable getTask() {
      // 从阻塞队列中拿一任务
        boolean timedOut = false; 

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 线程池关闭 && 队列为空,不需要再运行了,直接返回一个null;
            // 关闭这个线程,返回null
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // 线程数量是否超过了核心线程数,或者超时?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
						// 在这一个条件,如果这个线程是一个核心线程,就不会进入
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
						// 只有核心线程会执行到这里,尝试从工作队列获取任务,如果没有任务的话就不返回,继续无限循环拿一个任务,或者直到这个线程池被关闭;
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }