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

lingyuli
发布于 2020-9-9 16:18
浏览
0收藏

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

 

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

 

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,并在内部使用有界队列的方式或通过其他手段达到类似有界的效果。对于队列满时的饱和策略除了文中介绍的四种实现,同样可以根据实际情况自定义。

 

来源:InfoQ 

分类
已于2020-9-9 16:18:29修改
收藏
回复
举报
回复
    相关推荐