任务执行服务的主要实现机制:线程池

定义

线程池,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务执行完一个任务后不会退出,而是继续等待或执行新任务。

线程池主要是由两个概念组成:一个是任务队列;另一个是工作者线程。工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。

线程池的优点是显而易见的:

(1) 它可以重用线程,避免线程创建的开销。

(2)任务过多时,通过炮队避免创建过多的线程,减少系统资源消耗和竞争,确保任务有序完成。

理解线程池的一些参数

主要的构造方法

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

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,
                              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,
                              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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数corePoolSize、maximumPoolSize、keepAliveTime、unit用于控制线程池中线程的个数,workQueue 表示任务队列,threadFactor 用于对创建的线程进行一些配置,handler 表示任务拒绝策略。

线程池大小

maximumPoolSize 表示线程池的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值最大的线程个数。

corePoolSize 表示线程池中的核心线程个数,不过,并不是一开始就创建这么多线程,刚创建一个线程池后,实际上并不会创建任何线程。

一般情况下,有新任务到来的时候,如果当前线程个数小于corePoolSize,就会创建一个新线程来执行该任务,需要说明的是,即使其他线程现在也是空闲的,也会创建新的线程。不过,如果线程个数大于等于corePoolsize,那就不会立即创建新线程了,它会先尝试排队,需要强调的是,它是“尝试排队”,而不是“阻塞等待” 入队,如果队列满了或其他原因不能立即入队,它就不会排队,而是检查线程的个数是否达到了maximumPoolSize ,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize.

keepAliveTime 的目的是为了释放多余的线程资源,它表示,当线程池中的线程个数大于corePoolSize时,额外空闲线程的存活时间。也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。如果该值为0,则表示所有线程都不会超时终止。

队列

ThreadPoolExecutor 要求的队列类型是阻塞队列BlockingQueue。

比如:

LinkedBlockingQueue:基于链表的阻塞队列,可以指定最大长度,但默认是无界的。

ArrayBlockingQueue:基于数组的有界阻塞队列。

PriorityBlockingQueue:基于堆的无界阻塞优先级队列。

SynchronousQueue:没有实际存储空间的同步阻塞队列。

如果用的是无界队列,需要强调的是,线程个数最多只能达到corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize 也就没有意义了。

对于SynchronousQueue,我们知道,它没有实际存储元素的空间,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。

任务拒绝策略

如果队列有界,且maximumPoolSize 有限,则当队列排满,线程个数也达到了了maximumPoolSize,这时,新任务来了,如何处理呢?此时,会触发线程池的任务拒绝策略。

提交任务的方法(execute/submit/invokeAll)会抛出异常,类型为RejectedException。

ThreadPoolExecutor实现了四种处理方式:

(1)ThreadPoolExecutor.AbortPolicy:这就是默认的方式没跑出异常。

(2)ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行。

(3)ThreadPoolExecutor.DiscardOldestPolicy: 将等待时间最长的任务扔掉,然后自己排队。

(4)ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中线程执行。

它们都是ThreadPoolExecutor 的public静态内部类,都实现了RejectedExecutionHandler接口,这个接口的定义为:

public interface RejectedExecutionHandler {

    /**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

默认的RejectedExecutionHandler是一个AbortPolicy 实例。如下:

private static final RejectedExecutionHandler defaultHandler =
        new 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());
        }
    }

拒绝策略只有在队列有界,且maximumPoolSize 有限的情况下才会触发。如果队列无界,服务不了的任务总会排队,但这不一定是期望的结果,因为请求处理队列可能会消耗非常大的内存,甚至引发内存不够的异常,如果队列有界,但maximumPoolSize 无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完成。所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。

线程工厂

线程池还可以接受一个参数,ThreadFactory。它还是一个接口,定义为:

public interface ThreadFactory {

    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

根据Runnable 创建一个Thread。 ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory,主要就是创建一个线程,给线程设置一个名称,设置daemon属性为false,设置线程优先级为标准默认优先级,线程名称的格式为:pool-<线程池编号>-thread-<线程编号>。如果需要自定义一些线程的属性,比如名称,可以实现自定义的ThreadFactory。

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

核心线程的特殊配置:

线程个数小于等于corePoolSize时,我们称这些线程为核心线程,默认情况下:

(1) 核心线程不会预先创建,只有当有任务时,才会创建。

(2)核心线程不会因为空闲而被终止,keepAliveTime 参数不适用于它。

//预先创建所有线程
public int prestartAllCoreThreads() 

// 创建一个核心线程,如果所有核心线程都已创建,则返回false
public boolean prestartCoreThread() 

// 如果参数为true,则keepAliveTime 参数也适用于核心线程
public void allowCoreThreadTimeOut(boolean value)

工厂类Executors

提供了一些静态的工厂方法,可以方便地预创建一些线程池

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

只使用一个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。

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

使用固定数目的n个线程,使用无界队列LinkedBlockingQueue,线程创建后,不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗过多的内存。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

它的corePoolSize为 0 ,maximumPoolSize 为Integer.Max_Value, keepAliveTime 是60秒,队列为SynchronousQueue。它的含义是:当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果60秒内没有新任务,就终止。

实际中,应该使用newFixedThreadPool 还是newCachedThreadPool呢?

在系统负载很高的情况下,newFixedThreadPool 可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而newCachedThreadPool 会为每个任务创建一个线程,导致创建过多的线程竞争CPU和内存资源,使得任何实际任务都难以完成,这时,newFixedThreadPool 更为适用。

不过,如果系统负载不是太高,单个任务的执行时间也比较短,newCachedThreadPool 的效率可能更高,因为任务可以不经排队,直接交给某一个空线程。

在系统负载可能极高的情况下,两者都不是好的选择,newFixedThreadPool的问题是队列过长,而newCachedThreadPool的问题是线程过多,这时,应根据具体情况自定义ThreadPoolExecutor,传递合适的参数。

线程池的死锁

任务之间有依赖,这种情况可能会出现死锁。比如任务A,在它的执行过程中,它给同样的任务执行服务提交了一个任务B,但需要等待任务B结束。

如果任务A是提交了一个单线程池,一定会出现死锁,A在等待B的结果,而B在队列中等待调度。如果是提交给了一个限定线程个数的线程池,也能因线程数限制出现死锁。

怎么解决这种问题呢?可以使用newCachedThreadPool创建线程池,让线程数不受限制。另一个解决方法是使用SynchronousQueue,它可以避免死锁,怎么做到的呢?对于普通队列,入队只是把任务放到了队列中,而对于SynchronousQueue来说,入队成功就意味着已有线程接受处理,如果入队失败,可以创建更多线程直到maximumPoolSize,如果达到了maximumPoolSize,会触发拒绝机制,不管怎么样,都不会死锁。

参考文章

java编程的逻辑基础(马俊昌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章