Java核心技术19 | 并发库中的线程池

通常都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的ExecutorService类型或者不同的初始参数。

Executors目前提供了5种不同的线程池创建配置:

  • newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
  • newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
  • newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
  • newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
  • newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

Executor框架的基本组成

各个类型的设计目的

  • Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦。下面是其定义的唯一方法。
  void execute(Runnable command);
  • ExecutorService则更加完善,不仅提供service管理,比如shutdown等方法,也提供了更加全面的提交任务机制,如返回Future而不是 void 的 submit 方法。
 <T> Future<T> submit(Callable<T> task);

注意,这个例子输入的可是Callable,它解决了Runnable无法返回结果的困扰。

  • Java标准类库提供了几种基础实现,比如 ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool。这些线程池设的设计特点在于其高度的可调节性和灵活性,以尽量满足复杂多变的实际应用场景。
  • Executors则从简化使用的角度,为我们提供了各种方便的静态工厂方法。
  • 阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让我们更加明确线程池的运行规则,规避资源耗尽的风险。

下面分析线程池的设计与实现,主要围绕最基础的ThreadPoolExecutor源码。ScheduledThreadPoolExecutor是ThreadPoolExecutor的扩展,主要是增加了调度逻辑。而ForkJoinPool则是为了ForkJoinTask定制的线程池,与通常意义的线程池有所不同。

线程池的交互和线程池内部的工作过程参考下图:

 

 简单理解一下:

  • 工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为0的 SynchronousQueue(使用newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用LinkedBlockingQueue。
  private final BlockingQueue<Runnable> workQueue;
  • 内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会闲置一段时间(默认60秒)后结束线程。
 private final HashSet<Worker> workers = new HashSet<Worker>();

  线程池的工作线程被抽象为静态内部类Worker,基于AQS实现。

  • ThreadFactory提供上面所需要的创建线程逻辑。
  • 如果任务提交时被拒绝,比如线程池已处于SHUTDOWN状态,需要为其提供处理逻辑,Java 标准库提供了类似 ThreadPoolExecutor.AbortPolicy 等默认实现,也可以按照实际需要自定义。

从上面的分析,就可以看出线程池的几个基本组成部分,一起都体现在线程池的构造函数中:

  1. corePoolSize, 稍微的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如newFixedThreadPool 会将其设置为 nThreads ,而对于newCachedThreadPool则设为0。
  2. maximumPoolSize,顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对于newFixedThreadPool,当然就是nThreads,以为其要求是固定大小,而newCachedThreadPool则是Integer.MAX_VALUE。
  3. keepAliveTime和TimeUnit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。
  4. workQueue,工作队列,必须是BlockingQueue。

通过配置不同的参数,我们就可以创建出行为大相径庭的线程池,这就是线程池高度灵活性的基础。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

线程池有生命周期,状态如何表征?

ctl变量被赋予了双重角色,通过高低位的不同,既表示线程池状态(runstate),又表示工作线程数目(workerCount),这是一个典型的高效优化。试想,实际系统中,虽然我们可以指定线程极限为Integer.MAX_VALUE,但是因为资源限制,这只是个理论值,所以完全可以将空闲位赋予其他意义。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 真正决定了工作线程数的理论上限 
private static final int COUNT_BITS = Integer.SIZE - 3;       // 等于29,即workerCount所占的位数
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;  // 2^29-1
// 线程池状态,存储在数字的高位
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;
…
// Packing and unpacking ctl
private static int runStateOf(int c)  { return c & ~COUNT_MASK; }   // 获取高3位值
private static int workerCountOf(int c)  { return c & COUNT_MASK; } // 获取低29位值
private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池的状态有5种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。如果用二进制表示的话,这5种状态至少需要3位来表示,而一个int类型的数占32位,所以高3位表示线程状态,剩下的29位表示线程数量,故线程数量最大值为2^29-1。

至于为什么要低29位来保存workerCount,因为线程状态这个数据只关心能否表示5个值,并不关心具体数值,而workerCount是需要计算具体数据的,所以低29位可以直接拿来用,并不需要额外的运算。如果用高29位来表示workerCount,还需要进行右移操作才能取得真正的数值。

线程声明周期的状态转换图:

execute方法:

public void execute(Runnable command) {
…
	int c = ctl.get();
// 检查工作线程数目,低于 corePoolSize 则添加 Worker
	if (workerCountOf(c) < corePoolSize) {
    	if (addWorker(command, true))
        	return;
    	c = ctl.get();
	}
// isRunning 就是检查线程池是否被 shutdown
// 工作队列可能是有界的,offer 是比较友好的入队方式
	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);
	}
// 尝试添加一个 worker,如果失败意味着已经饱和或者被 shutdown 了
	else if (!addWorker(command, false))
    	reject(command);
}

线程池实践

线程池虽然提供了非常强大方便的功能,但是使用不得当也会导致问题。

  •  避免任务堆积。前面说过 newFixedThreadPool 是创建指定数目的线程,但是其工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现 OOM。
  • 避免过度扩展线程。我们通常在处理大量短时任务时,使用缓存的线程池。我们在创建线程池的时候,并不能准确预计任务压力有多大、数据特征是什么样子,所以很难明确设定一个线程数目。
  •  尽量避免在使用线程池时操作 ThreadLocal,工作线程的生命周期通常都会超过任务的生命周期。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值