ThreadPoolExecutor简析

ThreadPoolExecutor是java线程池的实现类,主要作用有这么几点:
1. 解耦任务提交和执行,便于控制任务执行的环境,用于吞吐量、响应能力等系统指标的调优;
2. 控制线程数量,复用线程,避免大量的线程造成资源竞争激烈,内存消耗严重引发的吞吐量下降、服务器宕机等问题;
3. 使系统稳定,当大量任务提交时,超过线程限制的任务会进入队列等待,达到平缓降低性能的目的。
下面我们来看看具体的实现。

线程池实现思路

我们知道每个线程都有线程空间也就是线程栈,里面放着栈帧(操作数栈、局部变量表等),随着方法的运行结束,那么线程所占用的资源也将被系统回收。
如何保证线程不被系统回收,而是人为的控制是否被回收呢?
循环是一个非常好的思路,只要线程不跳出循环,就不会被回收。我们通过控制什么时候线程跳出循环,来达到关闭线程的目的。具体怎么控制,实现思路应该很多,例如在循环中wait,等待任务到来然后notify唤醒线程,或者通过判断线程中断位,来跳出循环等等。
看看java中的ThreadPoolExecutor是如何实现的
通过以下我修剪后的代码,可以看到,差不多就是上面说的那些思路,java中用的是阻塞队列的超时和挂起来控制线程的生命周期。

//这个是运行任务的具体方法
final void runWorker(Worker w) {
       Runnable task = w.firstTask;
       while (task != null || (task = getTask()) != null) //当getTask返回null时,循环跳出,线程回收
            task.run();       
 }
 //获取任务
 private Runnable getTask() {
         boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; //判断线程是否可以超时回收
         Runnable r = timed ?
              workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //通过keepAliveTime来控制线程空闲死亡时间
              workQueue.take();//如果线程不允许回收,那么就会一直挂起
          if (r != null)
              return r;
}

下面我们来分析下,如何控制线程数,毕竟线程池的目的之一就是避免线程数的失控。
ThreadPoolExecutor主要有三个参数来控制:corePoolSize、maximumPoolSize、workQueue,那么具体是如何控制的呢?

  1. 当前线程总数小于corePoolSize时,不管有没有线程闲置,都会为新的任务创建新的线程,不会进行线程的复用;
  2. 当线程数等于corePoolSize,新到来的任务会进入队列中等待,等待的任务会被将来空闲的线程执行;
  3. 如果线程数等于corePoolSize且队列已经满了,会new新线程,直到线程数量等于maximumPoolSize;
  4. 当上面条件都验证后,那么在到来的任务就会进入拒绝服务,是直接放弃,还是抛出异常等各种策略可以定制。

首先我们看看实现具体的代码:

public void execute(Runnable command) {
        int c = ctl.get(); //ctl是atomicInteger类型,通过不同的计算方式得到线程池状态和线程数量
        if (workerCountOf(c) < corePoolSize) { //线程总数小于corePoolSize
            if (addWorker(command, true)) //为新的任务创建新的线程
                return;
        }
        if (isRunning(c) && workQueue.offer(command)) { //不能新建线程,那么就入队列(offer会立刻返回,失败为false)
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) //创建超过corePoolSize数量的线程
            reject(command);
    }

java提供的api使用

通过Executors提供的静态工厂方法,我们可以简单的使用线程池。

方法介绍
newFixedThreadPool(int nThreads)固定线程的线程池,任务会堆积在队列等待执行
newSingleThreadExecutor()一个线程的线程池,任务在队列中等待串行执行
newCachedThreadPool()最大线程数为Integer.MAX_VALUE的线程池,没有队列缓冲,不能被现有线程接受,直接创建新线程

以上线程池直接使用有没有问题?从上面介绍来看,好像问题很大,不管是任务过多的堆积,或是过多的创建线程。

1. newCachedThreadPool线程池
使用的是SynchronousQueue队列,offer方法直接把任务交个等待的线程,如果没有空闲线程挂起在队列上,那么直接返回false,从这里可以看出,当任务量急剧增大时,这可能导致线程无限增长,而线程创建是需要内存保存线程上下文信息的,可能会直接导致服务器内存不足,造成内存泄漏,同时线程对cup激烈的竞争,会导致服务器性能急剧下降。
2. newFixedThreadPool和newSingleThreadExecutor线程池
线程最大数量固定,通过LinkedBlockingQueue(容量很大的阻塞队列)来缓冲不能处理的任务,能够很好的保证合适数量的线程处于忙碌状态,但是同样会使得任务过多的堆积,导致响应能力下降。
从上分析看来,好像我们并不能直接的使用java提供的线程池啊,如果我们自己去实例化ThreadPoolExecutor会不会更好呢,那么我们就可以更精确的控制线程数量和队列长度,来达到更优的平衡。

个性化定制

通过有界队列、线程数量和空闲死亡时间来控制线程池,能够构造具有很好伸缩性的线程池,但是在系统资源使用和响应能力之间进行平衡,进行调优,却是件非常困难的事情。

  1. 使用大容量队列,小数量线程,虽然可以减少cup竞争,上下文切换,人为的控制过少的线程,同样会造成吞吐量的下降;
  2. 使用小容量队列,大数量线程,会使得cup竞争激烈,上线文频繁切换,也会使吞吐量下降。

参数的确定非常的困难,需要根据具体的场景,考虑任务的执行时间,响应要求进行合理的调整,这更是需要耐心测试的事情。
由此看来使用api直接提供的线程池也是一个选择,只有当确实出现性能瓶颈时,才需要更复杂的调优,而调优,却是高级技术。

怎么关闭线程池

我们知道,只要有非后台线程运行,那么虚拟机就不会关闭。只需要关闭每个线程,就能关闭线程池,不过,关闭线程池是个很有技巧的活。
ThreadPoolExecutor提供了2个方法

方法介绍
shutdown()平缓的关闭线程池
shutdownNow()暴力关闭线程池

何为平缓,何为暴力?分析前前我们列举下线程池状态,以及代表的意思(括号中的数字就可以认为就是状态的常量值):

状态介绍
RUNNING(-1)运行状态:接受新的任务或者运行队列中等待的任务
SHUTDOWN(0)关闭状态:不接受新的任务但运行队列中等待的任务
STOP(1)暴力关闭状态:不接受新任务且不执行队列中任,设置所有线程中断状态
TIDYING(2)当所有的任务都完成,所有的线程都消亡
TERMINATED(3)线程池完全关闭状态

先看看shutdown()的源码:

   public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN); //设置线程池状态
            interruptIdleWorkers(); //中断所有线程
            onShutdown();
        } finally {
            mainLock.unlock();
        }
        tryTerminate(); //尝试终止线程池
    }

从源码可以看到,shutdown方法主要是通过中断线程来关闭线程池的,什么时候线程会响应中断,从前面线程执行的分析可以看到,响应中断的情况主要是获取任务的时候挂起在队列上的操作take或者poll,如果队列中有任务,那么线程并不会响应中断,会继续执行,只有队列中没有任务时,线程挂起才会响应中断,跳出循环,可以去看看runWorker(Worker w)方法,可以看到每一次跳出循环就代表线程死亡,都会尝试关闭线程池,但是正常的关闭需要满足两个条件:队列为空且线程池状态大于SHUTDOWN。
由此可以总结,shutdown方法关闭的流程是:拒绝新任务加入–>执行完成所有剩余的任务–>关闭线程池

现在来看看 shutdownNow()的源码:

   public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP); //线程池状态为stop
            interruptWorkers();
            tasks = drainQueue(); //排干所有队列中等待的任务
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

对比shutdown的源码,我们发现状态变化为stop外,还多一个步骤,排干所有队列中的任务,让所有线程取不到任务,从而响应中断,快速地关闭线程池。这样的会存在很大的风险,但是风险的存在是看使用场景的,自己能够很好的把控风险,用这个方法也没有什么不可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值