1

使用线程池有哪些好处

1、降低系统开销

这一点是最容易想到的原因。如果系统为每个新的处理请求都创建一个线程,并在执行完任务后销毁该线程,会造成比较大的系统开销。甚至,操作线程的开销会大于执行任务本身的开销。
此时,如果用线程池来维护一批线程反复使用,会省去反复创建和销毁线程的开销。

2、控制最大并发线程数量

在没有线程池的状况下,系统会为每个请求创建一个线程。这种状况下,如果服务器瞬时接收到的请求量极大,系统中并发存在的线程会非常多。cpu会忙于创建和销毁线程,以及线程的上下文切换。使得线程的处理能力下降,造成大量线程等待甚至阻塞。
利用线程池,一方面可以控制线程总数,以防止线程数无休止的暴涨而形成的灾难性阻塞;另一方面,可以将过多的同时到达任务放入队列等待,以控制该类型请求对cpu最大的占用率,而避免对其他类型请求造成影响;甚至,可以引入拒绝策略,丢卒保车,舍弃一部分的请求换取服务器的安全运行。

3、功能强化

利用线程池的特性,可以实现任务延迟执行、定时执行、周期执行等花式操作…

本文将要介绍的是java.util.concurrent包下的ThreadPoolExecutor线程池,主要内容有以下几个部分:

  1. 基本属性和构造方法
  2. 四种队列
  3. 四种拒绝策略
  4. 四种常用的线程池
  5. 源码分析

以下部分从应用的角度介绍线程池,偏向于探究的源码分析在最后边

ThreadPoolExecutor的属性和构造方法

public class ThreadPoolExecutor extends AbstractExecutorService {
    /* 7个构造方法中的属性 */
    private volatile int corePoolSize;
    private volatile int maximumPoolSize;
    private volatile long keepAliveTime;
    private final BlockingQueue<Runnable> workQueue;
    private volatile ThreadFactory threadFactory;
    private volatile RejectedExecutionHandler handler;

    /* 1个常用的属性 */
    private volatile boolean allowCoreThreadTimeOut;
 
    /* 4个构造方法 */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}

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

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

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

}

可见,ThreadPoolExecutor的构造方法爸爸们实际是对第1个构造方法爷爷的封装,而构造方法爷爷送上了7个重要概念:

  • corePoolSize 核心池容量
  • maximumPoolSiz 最大池容量
  • keepAliveTime 线程存活时间
  • TimeUnit 时间单位
  • BlockingQueue<Runnable> workQueue 阻塞队列
  • ThreadFactory 线程工厂
  • RejectedExecutionHandler 拒绝策略执行控制器

看到corePool这个概念,一定能想到是不是还有非core的pool。答案是有的,一个池子里的线程是这样分堆儿的:

核心pool容量(corePoolSize) + 非核心pool容量 = 总容量(maximumPoolSiz)

由于这种机制,于是就产生了如下的神奇效果:

  1. 当调度进程送了一个新的任务进入线程池时,会检查核心线程池中是否有空闲的线程,如果有直接拿来用,如果没有进入步骤二;
  2. 检查一下核心线程池中的线程数量是否小于corePoolSize,如果是则建一个新线程放入核心线程池中,并拿来用,如果已经满了进入步骤三;
  3. 检查一下阻塞队列是不是满了,如果没有,将新任务放到队尾,排队等待核心线程池来执行,如果满了进入步骤四;
  4. 检查一下非核心线程池是否有空闲线程,如果有直接拿来用,如果没有进入步骤五;
  5. 检查一下,全部线程数是否小于maximumPoolSiz,如果是,创建一个带有过期时间的新线程放入线程池并拿来用,如果满了,恭喜你是最倒霉的一个任务。

当然,上边的过程只是基于corePoolSizemaximumPoolSizkeepAliveTime三个属性产生的最基本的逻辑。并且,在默认状态下allowCoreThreadTimeOut = false,核心线程会一直存活下去,而从创建的第corePoolSize+1个线程开始,线程池会创建带有一个由keepAliveTimeTimeUnit决定的存活时间的非核心线程。当非核心线程的空闲时间超过keepAliveTime时,该线程将被销毁。
而在非默认状态下,也就是当allowCoreThreadTimeOut = true时,线程池中所有的线程都受keepAliveTime的控制,都具有过期销毁的特性

ThreadPoolExecutor的四种队列

我们来看一下BlockingQueue<Runnable> workQueue这个阻塞队列属性。从上文中我们可以知道,workQueue的作用就是维护等待核心线程执行的任务所组成的工作队列。下面我们将介绍4种常用的工作队列:

  • 1. SynchronousQueue

    • 源码说法:Direct handoffs. A good default choice for a workqueue is a SynchronousQueue that hands off tasks to threads without otherwise holding them. Here, an attempt to queue a task will fail if no threads are immediately available to run it, so a new thread will be constructed. This policy avoids lockups when handling sets of requests that might have internal dependencies. Direct handoffs generally require unbounded maximumPoolSizes to avoid rejection of new submitted tasks. This in turn admits the possibility of unbounded thread growth when commands continue to arrive on average faster than they can be processed.
    • 简单来说:这是一个直接拒绝队列,也就是任何任务都不会被放到队列里去排队。既然没有队列,那么所有提交给池子的任务,都会被直接交给线程去执行。先找核心线程,再找非核心线程。但是,这是会有一个关键的问题:如果池子里没有空闲的线程是不是要抛异常了?所以,当我们选择使用SynchronousQueue时,通常会将maximumPoolSize指定Integer.MAX_VALUE,即无限大。但是此时要注意,当瞬时并发量暴增时,池子中的线程数会非常大。此时,如果你不想使用队列,又不想让线程数失控,可以指定maximumPoolSize,并面对新任务执行失败的风险。
  • 2. LinkedBlockingQueue:

    • 源码说法:Unbounded queues. Using an unbounded queue (for example a LinkedBlockingQueue without a predefined capacity) will cause new tasks to wait in the queue when all corePoolSize threads are busy. Thus, no more than corePoolSize threads will ever be created. (And the value of the maximumPoolSize therefore doesn't have any effect.) This may be appropriate when each task is completely independent of others, so tasks cannot affect each others execution; for example, in a web page server. While this style of queuing can be useful in smoothing out transient bursts of requests, it admits the possibility of unbounded work queue growth when commands continue to arrive on average faster than they can be processed.
    • 简单来说:这是一个无界队列。当使用了这个队列时,如果核心线程池满了,新的任务会送入队列中排队,并且这个队列是没有长度限制的。可见,非核心线程是永远不会出现的,总线程数永远不会超过corePoolSize,所以maximumPoolSize的设定也就失效。这种用法下,线程数是受控的,只要内存不溢出新的任务就有机会被最终执行。不完美的是,当瞬时并发量大时,队列中任务会较多,等待时间会较长,也存在OOM风险。
  • 3. ArrayBlockingQueue:

    • 源码说法:Bounded queues. A bounded queue (for example, an ArrayBlockingQueue) helps prevent resource exhaustion when used with finite maximumPoolSizes, but can be more difficult to tune and control. Queue sizes and maximum pool sizes may be traded off for each other: Using large queues and small pools minimizes CPU usage, OS resources, and context-switching overhead, but can lead to artificially low throughput. If tasks frequently block (for example if they are I/O bound), a system may be able to schedule time for more threads than you otherwise allow. Use of small queues generally requires larger pool sizes, which keeps CPUs busier but may encounter unacceptable scheduling overhead, which also decreases throughput.
    • 简单来说:这是一个有界队列,该队列有指定的长度。使用这个队列时,遵从上边典型的5步过程。
  • 4. DelayQueue:

    • 简单来说:这是一个延时队列,进入该队列的任务必须实现Delayed接口。如此,进入了这个队列的任务,只有达到了指定的延时时间,才会被执行。

ThreadPoolExecutor的四种拒绝策略

下面,我们再来看一下属于倒霉蛋的礼物--拒绝策略。在线程池源码中,设计者为我们提供了四种拒绝策略,分别如下:

  • 1. AbortPolicy 默认的拒绝策略

    • “好人卡策略”,线程池最近的约会太多了,排满了队列,于是它委婉的拒绝了你的邀请。直接拒绝新任务进入线程池,并丢弃该任务,同时发一张叫RejectedExecutionException的好人卡。
    • 适合并发量受限,且比较重要的业务场景,可以通过抛出异常的情况,对被丢弃的任务做出实弹的处理。手握好人卡,再试一次说不定可以成功
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  • 2. CallerRunsPolicy:

    • “自助餐策略”,餐厅大厨说,太忙了,今天的饭谁爱做谁做。于是,老板开了一家自助餐厅。线程池爸爸让调度这条任务的线程自己去干。注意哦,并发激增的话,线程数量也会激增,你的客人挤爆了你的餐厅。
    • 适合比较重要的,必须被执行的任务。吃自助的人如果太多、饭量太大,可能会赔钱哦。
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
  • 3. DiscardPolicy:

    • “养大爷策略”,线程池说他同时只能排上这么多会儿,多出来的他不干,也懒得告诉你。于是,向你丢出一堆溢出的任务,并一句话也不说。好处就是能防止线程数量激增。
    • 适合不重要的业务场景做兜底方案,线程数量可以收到控制。我不知道该说些什么,只是隐隐觉得哪里不对劲。
     public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
  • 4. DiscardOldestPolicy:

    • “九房姨太策略”,线程池只有九间房子给老婆们,想要娶第十个怎么办,只能休了大姨太。丢弃队列头部的任务,将新任务放入队尾。
    • 适合哪种业务场景,你开心就好。
     public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

Executors提供的四种常用线程池

java.util.concurrent包中的Executors类中提供了4中通过包装ThreadPoolExecutor实现的线程池。当然,也有通过其他方法实现的线程池,这部分有机会另做分享。下面我们来逐一看一下,由ThreadPoolExecutor封装的线程池。

  • 1. newFixedThreadPool 固定大小线程池

    • 每种线程池都提供了两个初始化方法,但是需要自己传入threadFactory的这个方法用的不多,后边不再给出。
    • 从参数配置上可以看出,该线程池的corePoolSizemaximumPoolSiz一样,工作队列使用的是无界队列。也就是说,线程池中固定存在nThreads个线程,并且不能被立即执行的任务都会进入队列排队。
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 2. newSingleThreadExecutor 单线程线程池

    • 该线程池中固定只有1个线程,工作队列使用的是无界队列,并且不能被立即执行的任务都会进入队列排队。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 3. newCachedThreadPool 缓存程线程池

    • 这是一个调皮的线程池,没有核心线程没有队列,只有非核心线程,每个线程固定存活60秒。并且,线程数量可以无限多。注意,有线程数量激增的风险哦。
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 4. newScheduledThreadPool 调度程线程池

    • 乍一看这个线程池似乎与ThreadPoolExecutor没有关系。实际上,ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,并实现了ScheduledExecutorService接口。
    • 从配置上看,该线程池只有corePoolSize个核心线程,并且使用延迟队列实现任务延迟执行。此外,通过ScheduledExecutorService接口方法,实现了任务的周期性执行。(这一部分以后会再单独分享)
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor implements ScheduledExecutorService {
        
        public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
}

踏水丶余痕
4 声望0 粉丝