0%

多线程编程 —— 线程池

如果本文有错误,希望指出。

在项目中,线程是一种稀缺资源,频繁的创建和销毁,对于系统的性能有着很大的消耗。线程池是线程资源复用的典范之作,通过维护一个一定数量的线程集合,在需要运行线程任务的时候直接从这个集合中取出一个线程去运行任务,而不是重新创建一个。这点在阿里Java手册上也提出了:

在Java中提供了几种线程池创建的方法,不过这几种方法都是最后通过 ThreadPoolExecutor 来创建线程的。在开始讲Java提供的几种之前,先讲下 ThreadPoolExecutor 中几个关键字的含义。

使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

ThreadPoolExecutor

内部工作原理

  • corePoolSize :池中所保存的线程数,包括空闲线程
  • maximumPoolSize:池中允许的最大线程数
  • keepAliveTime: 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
  • unit:keepAliveTime 参数的时间单位
  • workQueue :执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务
  • threadFactory:执行程序创建新线程时使用的工厂
  • handler :由于超出线程范围和队列容量而使执行

线程池运行思路

  • 如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务
  • 如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列
  • 如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务
  • 如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策略来处理该任务
  • 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出

在线程大于 corePoolSize,进入队列等待。如果队列也满了,且线程数小于 maximumPoolSize,则创建新的线程。如果线程数大于maximumPoolSize,则使用拒绝策略来处理任务。

拒绝策略

线程池有四种拒绝策略:

  • AbortPolicy:抛出 RejectedExecutionException 异常,默认
  • CallerRunsPolicy:不使用线程池执行
  • DiscardPolicy:直接丢弃任务
  • DiscardOldestPolicy:丢弃队列中最旧的任务

对于线程池选择的拒绝策略可以通过 RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); 来设置。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

Java 中提供的几种方法

newFixedThreadPool

通过创建一个 corePoolSizemaximumPoolSize 相同的线程池。使用LinkedBlockingQuene作为阻塞队列,不过当线程池没有可执行任务时,也不会释放线程。

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

newCachedThreadPool

  • 初始化一个可以缓存线程的线程池,以前构建的线程可用时将重用它们,空闲线程默认缓存60s,线程池的线程数可达到 Integer.MAX_VALUE,即 2147483647,内部使用 SynchronousQueue 作为阻塞队列;
  • newFixedThreadPool 创建的线程池不同,newCachedThreadPool 在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
    1
    2
    3
    4
    5
    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }

newSingleThreadExecutor

初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列。

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

newScheduledThreadPool

初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。除了newScheduledThreadPool的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类实现的.

1
2
3
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

newWorkStealingPool

newWorkStealingPool,它是 jdk1.8 提供的一种线程池,用于执行并行任务。默认并行级别为当前可用最大可用cpu数量的线程。

使用场景:用于大耗时同时可以分段并行的任务。

1
2
3
4
5
6
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}

注意

除了 newWorkStealingPool 之外,其他四个创建方式都存在资源耗尽的风险。在阿里Java开发手册上面关于线程池创建的注意:

参考

深入分析java线程池的实现原理
Github上的源码

客官,赏一杯coffee嘛~~~~