Java 并发编程系列——线程池

用户头像
孙苏勇
关注
发布于: 2020 年 05 月 04 日
Java并发编程系列——线程池

之前写了线程和锁,例子中采用直接创建线程的方式,这种方式做示例可以,但在实际生产环境中比较少用,通常会使用线程池。



使用线程池有一些明显的好处,可以考虑我们使用连接池的情形,不难想像。使用线程池可以免去我们手动创建和销毁线程的工作,节省这部分资源的消耗,提高响应速度,同时线程由线程池维护,也提高了线程的可管理性。



JDK中默认实现了多种线程池,如FixedThreadPool,SingleThreadExecutor,CachedThreadPool,ScheduledThreadPool,SingleThreadScheduledExecutor,WorkStealingPool。ForkJoinPool也是线程池的一种,通常我们单独讨论,之前的文章有所介绍。



线程工厂



线程池的创建方法使用线程池的工厂类Executors,调用相应的方法创建相应的线程池。创建线程池的工作即实例化ThreadPoolExecutor,所以有必要简单看下ThreadPoolExecutor。



ThreadPoolExecutor的构造参数:

/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

对如上参数简单说明:



corePoolSize:线程池中的核心线程数。也就是线程池会长期保持的线程数。当通过线程池执行任务时,如果当前的线程数小于corePoolSize,则创建新线程,如果当前线程数已经为corePoolSize,则任务进入工作队列。如果希望一次性创建出核心线程,调用prestartAllCoreThreads()。



maximumPoolSize:池中允许的最大线程数。当核心线程数满,并且阻塞队列也满了,此时如果池中的线程数仍小于maximumPoolSize,则会创建新的线程。



keepAliveTime:空闲线程存活的时间。仅当池中线程数大于corePoolSize时有效,也就是在上述maximumPoolSize所讲条件触发创建线程后,使得池中线程大于核心线程数后,才会根据该条件来销毁线程。



unit:时间单位



workQueue:保存尚未执行的任务的阻塞队列



threadFactory:创建线程的工厂



handler:饱和策略。也就是阻塞队列满了之后的处理方式。饱和策略有四种,AbortPolicy(抛出异常),CallerRunsPolicy(由调用线程直接执行,如果 调用线程已经销毁则丢弃),DiscardOldestPolicy(丢弃最早的任务,即队列头部的任务),DiscardPolicy(直接丢弃)。

任务的执行

用线程池执行任务有两种方式,execute和submit,execute无返回值,submit返回Future,而Future可以返回结果和接收异常。根据需要使用具体的方法。

线程池的停止

关闭线程池使用shutdown()和shutdownNow()。使用shutdown()时,尚未被执行的任务将不再执行,而已经在执行的任务将继续。使用shutdownNow()时,尚未被执行的任务将不再执行,并且会尝试停止正在运行的任务。



接下来简单介绍下几个常见线程池



FixedThreadPool:

corePoolSize等于maximumPoolSize,阻塞队列使用了LinkedBlockingQueue,但并未初始化其容量,可以认为相当于使用了无界队列(为什么说到无界,文章最后会提到)。适用于对服务器负载有严格控制的场景。



SingleThreadExecutor:

corePoolSize等于maximumPoolSize等于1,阻塞队列同样使用了未初始化容量的LinkedBlockingQueue,为无界队列。适用于对任务执行顺序有要求的场景。



CachedThreadPool:

corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,使用的队列为SynchronousQueue,同样为无界。该线程池会根据需要创建新线程,适用于执行时间非常短的数量较多的异步任务。



ScheduledThreadPool:

其maximumPoolSize为Integer.MAX_VALUE,队列使用了DelayedWorkQueue,同样为无界。适用于需要定期执行任务的场景。



SingleThreadScheduledExecutor

corePoolSize为1,队列同样使用了DelayedWorkQueue,为无界。适用于需要定期按顺序执行任务的场景。



WorkStealingPool:

工作密取队列,内部使用了ForkJoinPool,但使用了默认工厂创建,同样为无界形式。



线程池使用示例



通过一段代码简单看下线程的使用,以SingleThreadExecutor为例。

public class ShowSingleExecutor {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
System.out.println("executed 1 by single thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.execute(() -> {
System.out.println("executed 2 by single thread");
}
);
executorService.shutdown();
}
}



该示例将演示使用单线程线程池,其第一个任务执行5秒后才会执行第二个任务。



其他线程池文章中鉴于篇幅不再举例。



自定义线程池



最后说一下之前提到的无界问题。无界意味着如果出现处理不够及时的情况时,任务会逐渐堆积,而造成服务不可用或服务崩溃。所以在实际使用中通常需要自定义ThreadPoolExecutor,并在内部使用有界队列的方式或通过其他手段达到类似有界的效果。对于队列满时的饱和策略除了文中介绍的四种实现,同样可以根据实际情况自定义。



欢迎关注公众号“像什么”



本系列其他文章:

Java并发编程系列——分布式锁

Java并发编程系列——锁顺序

Java并发编程系列——锁

Java并发编程系列——常用并发工具类

Java并发编程系列——Fork-Join

Java并发编程系列——线程的等待与唤醒

Java并发编程系列插曲——对象的内存结构

Java并发编程系列——线程

发布于: 2020 年 05 月 04 日 阅读数: 1862
用户头像

孙苏勇

关注

不读书,思想就会停止。 2018.04.05 加入

公众号“像什么",记录想记录的。

评论

发布
暂无评论
Java并发编程系列——线程池