玩线程池 引发的思考

package org.ssm.king.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    public static void main(String[] args) {

        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(index);
                }
            });
        }
    }
}

第一次接触线程池,随便写了一个,发现问题了。其实,Executors是一个线程池的实现类,其中包括了四种常用线程池,点开源码,哇哦,里面有好几个参数,都代表什么意思呢?一脸懵逼状。。。下面先来分析线程池:

  • ThreadPoolExecutor父类是AbstractExecutorService

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

先来一一分析下这个构造函数中每个参数的意义:

  1. corePoolSize核心线程池大小,包括空闲线程。初创线程池时,线程不会立即启动,知道有任务提交才开始启动线程并逐渐使线程数量达到corePoolSize大小。若想要一开始就创建所有核心线程,需要调用方法prestartAllCoreThreads()。
  2. maximumPoolSize一目了然,该参数为线程池允许的最大线程数。当核心线程满且阻塞队列也满时才会判断当前线程是否小于最大线程数,并创建或否新的线程。
  3. keepAliveTime保持存活的时间,当前线程大于核心线程池数量时,多余的空闲线程最多存活时间。
  4. unit这个参数是keepAliveTime的时间单位。
  5. workQueue工作队列,当前线程超过核心线程数时用于保存任务的队列。主要有三种类型的阻塞队列:无界队列、有界队列以及同步移交队列。
  6. threadFactory创建线程的工厂,工厂模式了解一下。
  7. handler阻塞队列已满且线程数达到最大值时所采用的饱和策略。java默认提供了四种饱和策略的实现:中止、抛弃、抛弃最旧、调用者运行。

唉!又引入了新的问题。一起来研究研究这几个阻塞队列吧。然后再分析四种饱和策略。

可选择的阻塞队列BlockingQueue

先在这理一下新任务进入时线程池的执行策略:

如果运行的线程少于corePoolSize,添加新的线程。如果运行的线程大于corePoolSize,将请求加入阻塞队列中。如果无法加入队列,则创建新的线程,当超出maximumPoolSize,任务将拒绝。

  • 无界队列

无界的LinkedBlockingQueue,队列大小无限制,当任务耗时较长时,可能会导致大量新任务在队列中堆积,最终导致OOM(out of memory)。小心慎用。

  • 有界队列

常用两类:FIFO的队列ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类时优先级队列PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。使用有界队列时大小需要与线程池大小相互配合,线程池较小游街队列较大可减少内存消耗,降低cpu使用率和上下文切换,但相对的吞吐量会首先。优化永远是找平衡的一件事情。

  • 同步移交

如果不希望任务在队列中等待,而是将任务直接移交给工作线程,可以使用SynchronousQueue。上述例子就是用的该队列,SynchronousQueue并不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须由另一个线程正在等待接受这些元素,只有在使用无界线程池或者由饱和策略时才建议使用该队列。

聊聊饱和策略RejectedExecutionHandler吧

  • 先来看看默认的策略:AbortPolicy中止策略

    /**
     * A handler for rejected tasks that throws a
     * {@code 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());
        }
    }

使用该策略时,在饱和时会抛出RejectedExecutionException异常,调用者可以捕获该异常自行处理。

  • DiscardPolicy抛弃策略

    /**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    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) {
        }
    }

可以看到,方法rejectedExecution为空实现,当饱和时,不做任何处理直接抛弃任务。

  • DiscardOldestPolicy抛弃最老任务策略

    /**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries {@code execute}, unless the executor
     * is shut down, in which case the task is discarded.
     */
    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);
            }
        }
    }

咳咳,不要在意这些翻译细节。。。

在达到饱和时,阻塞队列的头元素抛弃,再尝试提交任务。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将导致优先级最高的任务被抛弃,因此不建议将该种策略配合优先级队列使用。

  • CallerRunsPolicy调用者运行策略

    /**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    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();
            }
        }
    }

该策略,看实现,当达到饱和时,不抛异常也不抛弃任务,直接运行run方法,这样做会将任务回退给调用者来直接运行。使用该策略时线程池饱和后酱油调用线程池的主线程自己来执行任务,因此再执行任务的这段时间里主线程无法再提交新的任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。

最后,再聊聊java的四种常用线程池。

  • newCachedThreadPool

    /**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

在newCachedThreadPool中如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 
初看该构造函数时我有这样的疑惑:核心线程池为0,那按照前面所讲的线程池策略新任务来临时无法进入核心线程池,只能进入 SynchronousQueue中进行等待,而SynchronousQueue的大小为1,那岂不是第一个任务到达时只能等待在队列中,直到第二个任务到达发现无法进入队列才能创建第一个线程? 
这个问题的答案在上面讲SynchronousQueue时其实已经给出了,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。因此即便SynchronousQueue一开始为空且大小为1,第一个任务也无法放入其中,因为没有线程在等待从SynchronousQueue中取走元素。因此第一个任务到达时便会创建一个新线程执行该任务。 
这里引申出一个小技巧:有时我们可能希望线程池在没有任务的情况下销毁所有的线程,既设置线程池核心大小为0,但又不想使用SynchronousQueue而是想使用有界的等待队列。显然,不进行任何特殊设置的话这样的用法会发生奇怪的行为:直到等待队列被填满才会有新线程被创建,任务才开始执行。这并不是我们希望看到的,此时可通过allowCoreThreadTimeOut使等待队列中的元素出队被调用执行。

  • newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

    /**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

看实现,线程池大小固定,队列采用无界队列。

  • newScheduledThreadPool创建一个定长线程池,支持定时及周期性执行任务

    /**
     * Creates a thread pool that can schedule commands to run after a
     * given delay, or to execute periodically.
     * @param corePoolSize the number of threads to keep in the pool,
     * even if they are idle
     * @return a newly created scheduled thread pool
     * @throws IllegalArgumentException if {@code corePoolSize < 0}
     */
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    /**
     * Creates a new {@code ScheduledThreadPoolExecutor} with the
     * given core pool size.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @throws IllegalArgumentException if {@code corePoolSize < 0}
     */
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值