线程池详解

71 阅读13分钟

前言

我们经常会通过启动线程在子线程处理耗时任务,但是每个线程的创建和销毁都需要一定的开销。并且每次通过new Thread().start()来启动线程,各个线程各自为政,很难对其进行控制。这时候就需要线程池来对线程进行管理。

线程池

什么是线程池?简单理解,就是一个管理线程的池子。线程池就是采用池化思想(类似连接池、常量池、对象池等)管理线程的工具。

使用线程池的好处:

  • 重用线程池中的线程。线程的创建和销毁的开销是巨大的,而通过重用线程池中的线程大大减少了这些不必要的开销。
  • 能有效控制线程池的最大并发数。避免大量线程之间因抢占系统资源而导致的阻塞现象。
  • 能够对线程进行简单的管理。线程池可以提供定时执行以及指定间隔循环执行等功能。

ThreadPoolExecutor

可以通过ThreadPoolExecutor来创建一个线程池,ThreadPoolExecutor一共有4个构造方法,另外3个构造方法都间接调用了参数最多的构造方法,参数最多的构造方法代码如下:

public class ThreadPoolExecutor extends AbstractExecutorService {

    public ThreadPoolExecutor(int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler) {
        ...
    }

}

其构造方法中参数的作用如下:

  • corePoolSize:线程池的核心线程数。默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态。除非你将参数allowCoreThreadTimeOut设置为true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime指定,当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止。
  • maximumPoolSize:线程池允许创建的最大线程数。当活动线程数达到这个数值后,后续的新任务将会被阻塞。
  • keepAliveTime:非核心线程闲置时的超时时长。超过这个时长,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,keepAliveTime同样会作用于核心线程。
  • unit:keepAliveTime的单位。这是一个枚举,常用的有 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES (分钟)等。
  • workQueue:线程池中的任务队列。通过线程池的execute()方法提交的Runnable对象会存储在这里,它是BlockingQueue类型的,也就是阻塞队列。
  • threadFactory:线程工厂。为线程池提供创建新线程的功能,一般情况下无需设置该参数,使用默认的就行。
  • handler:拒绝策略。当线程池无法执行新任务时,这可能是由于任务队列已满或者是无法成功执行任务,这个时候ThreadPoolExecutor会调用handler的rejectedExecution()方法来通知调用者。

ThreadPoolExecutor 为 RejectedExecutionHandler 提供了几个可选值:AbortPolicy、CallerRunsPolicy、DiscardPolicy 和 DiscardOldestPolicy,代码如下:

public class ThreadPoolExecutor extends AbstractExecutorService {

    /**
    * 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();
            }
        }
    }

    /**
    * 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());
        }
    }

    /**
    * 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) {
        }
    }

    /**
    * 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);
            }
        }
    }
} 
  1. AbortPolicy:默认值,直接抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:交给调用 execute() 方法的线程进行执行。如果 executor 已经 shutdown 了,则直接丢弃。
  3. DiscardPolicy:直接丢弃。
  4. DiscardOldestPolicy:丢弃最老的未处理的请求,然后重新执行 execute() 方法。如果 executor 已经 shutdown 了,则直接丢弃。

线程池的处理流程

ThreadPoolExecutor执行任务时大致遵循如下流程:

  1. 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
  2. 如果线程池中的线程数量已经达到或者超过核心线程的数量,那么新任务会被插入到任务队列中排队等待执行。
  3. 如果在步骤2中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到最大线程数,那么会立刻启动一个非核心线程来执行任务。
  4. 如果步骤3中线程数量已经达到最大线程数,那么就执行拒绝策略。

4种常用的线程池

下面介绍4种常用的线程池,它们都是直接或间接地通过配置ThreadPoolExecutor来实现自己的功能特性。

  1. FixedThreadPool,其代码如下:
public class Executors {

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

它通过Executors的newFixedThreadPool()方法来创建。它是一种线程数量固定的线程池,其核心线程数和最大线程数都被设置成nThreads。另外,由于任务队列是无界阻塞队列LinkedBlockingQueue(容量默认为Integer.MAX_VALUE),相当于不会产生非核心线程,keepAliveTime参数无效。当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了,这样,线程池中的线程可以得到重用。当所有的线程都处于活动状态时,新任务会被添加到LinkedBlockingQueue中等待,直到有线程空闲出来。由于FixedThreadPool只有核心线程并且这些核心线程不会被回收,这意味着它能够更加快速地响应外界的请求。

  1. CachedThreadPool,代码如下:
public class Executors {

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

通过Executors的newCachedThreadPool()方法来创建。它是一种线程数量不定的线程池,其核心线程为0,最大线程数为Integer.MAX_VALUE。由于Integer.MAX_VALUE是一个很大的数,实际上就相当于最大线程数可以任意大。当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会利用空闲的线程来处理新任务。线程池中的空闲线程的超时时长为60秒,超过60秒闲置线程就会被回收。CachedThreadPool的任务队列SynchronousQueue是一个不存储元素的阻塞队列,其每一个插入操作必须等待另一个线程相应的移除操作,相当于每次提交到任务队列的任务马上就会被线程池中的线程拿去执行。从CachedThreadPool的特性来看,这类线程池比较适合执行大量的耗时较少的任务。当整个线程池都处于闲置状态时,线程池中的线程都会超时而被停止,这个时候CachedThreadPool之中实际上是没有任何线程的,它几乎是不占用任何系统资源的。

3.SingleThreadExecutor,代码如下:

public class Executors {

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

通过Executors的newSingleThreadExecutor()方法来创建。线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。SingleThreadExecutor 的意义在于统一所有的任务到一个线程中,这使得在这些任务之间不需要处理线程同步的问题。

  1. ScheduledThreadPool,代码如下:
public class Executors {

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
}

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {

    private static final ong DEFAULT_KEEPALIVEMILLIS = 10L;

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
                DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
                new DelayedWorkQueue());
    }

}

通过Executors的newScheduledThreadPool()方法来创建。它的核心线程数是固定的,最大线程数是没有限制的,由于DelayedWorkQueue是无界的,所以最大线程数这个参数是无效的。ScheduledThreadPool 这类线程池主要用于执行定时任务和具有固定周期的重复任务。可以调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWithFixedDelay()方法来执行周期性的任务。下面是一个使用的实例:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("current time millis is:" + System.currentTimeMillis());
    }
};

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);

//2秒后执行runnable
scheduledThreadPool.schedule(runnable, 2, TimeUnit.SECONDS);

//第一个任务10ms后执行,后面每隔1000ms执行下一个任务
scheduledThreadPool.scheduleAtFixedRate(runnable, 10, 1000, TimeUnit.MILLISECONDS);

//第一个任务10ms后执行,后面的任务的执行时间是上一个任务执行结束后的1000ms
scheduledThreadPool.scheduleWithFixedDelay(runnable, 10, 1000, TimeUnit.MILLISECONDS);

一般不推荐使用Executors去创建线程池,而推荐使用ThreadPoolExecutor的方式,使用Executors去创建线程池的风险如下:

  1. FixedThreadPool和SingleThreadExecutor:允许的任务队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。

线程池的异常处理

我们先来看一段代码:

ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    threadPool.submit(() -> {
        System.out.println("current thread name:" + Thread.currentThread().getName());
        Object object = null;
        System.out.println("result:" + object.toString());
    });
}

执行后打印如下:

current thread name:pool-1-thread-1
current thread name:pool-1-thread-3
current thread name:pool-1-thread-4
current thread name:pool-1-thread-2
current thread name:pool-1-thread-5

显然上面的代码中object为null,执行object.toString()会有异常,但是打印并没有看到异常,这种情况下就没办法感知代码中的异常了。

线程池中处理异常的方式有4种:

  1. 添加try catch语句;
  2. 通过Future对象的get()方法接收抛出的异常进行处理;
  3. 给线程设置UncaughtExceptionHandler;
  4. 重写ThreadPoolExecutor的afterExecute()方法;

先来看看第1种方式,自己添加try catch语句:

ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    threadPool.submit(() -> {
        System.out.println("current thread name:" + Thread.currentThread().getName());
        try{
            Object object = null;
            System.out.println("result:" + object.toString());
        }catch (Exception e){
            System.out.println(Thread.currentThread().getName() + " 出异常了");
        }
    });
}

执行后打印如下:

current thread name:pool-1-thread-1
pool-1-thread-1 出异常了
current thread name:pool-1-thread-2
current thread name:pool-1-thread-4
current thread name:pool-1-thread-3
current thread name:pool-1-thread-5
pool-1-thread-4 出异常了
pool-1-thread-2 出异常了
pool-1-thread-5 出异常了
pool-1-thread-3 出异常了

第2种方式我们需要分析一下源码,看看为什么可以通过Future对象的get()方法拿到抛出的异常,我们从submit()方法开始跟一下源码:

public abstract class AbstractExecutorService implements ExecutorService {

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
}

submit()方法里面通过newTaskFor()方法将传入的Runnable对象包装成FutureTask,然后将这个FutureTask交给execute()方法执行,最后返回FutureTask。

classDiagram
    Future <|-- RunnableFuture
    Runnable <|-- RunnableFuture
    RunnableFuture <|.. FutureTask

    class Future {
        <<interface>>
        +cancel()
        +isCancelled()
        +get()
    }
    
    class Runnable {
        <<interface>>
        +run()
    }
    
    class RunnableFuture {
        <<interface>>
    }
    
    class FutureTask {
        +run()
        +get()
        +cancel()
    }

FutureTask可以用来包装Callable或Runnable对象,因为FutureTask实现了Runnable接口。

public class FutureTask<V> implements RunnableFuture<V> {

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW; // ensure visibility of callable
    }
}

在FutureTask的构造方法中,将Runnable包装成了一个Callable类型的对象。

public void run() {
    if (state != NEW ||
            !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

在FutureTask的run()方法中,调用了Callable对象的call()方法,也就调用了我们传入的Runnable对象的run()方法。这里可以看到有添加try catch语句,抛出的异常会传入setException()方法:

protected void setException(Throwable t) {
    if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
        outcome = t;
        U.putOrderedInt(this, STATE, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

在setException()方法中将异常赋值给了outcome。接着看看FutureTask的get()方法:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

里面又调用了report()方法:

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

在report()方法方法中会拿到这个outcome并抛出,所以可以通过Future的get()方法接收抛出的异常进行处理,将上面的示例改造一下:

ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    Future future = threadPool.submit(() -> {
        System.out.println("current thread name:" + Thread.currentThread().getName());
        Object object = null;
        System.out.println("result:" + object.toString());
    });

    try{
        future.get();
    }catch (Exception e){
        System.out.println("出异常了");
    }
}

打印如下:

current thread name:pool-1-thread-1
出异常了
current thread name:pool-1-thread-2
出异常了
current thread name:pool-1-thread-3
出异常了
current thread name:pool-1-thread-4
出异常了
current thread name:pool-1-thread-5
出异常了

第3种方式是通过给线程设置UncaughtExceptionHandler,然后在其uncaughtException()方法中处理异常,这种方式只能用在使用execute()提交任务的代码中:

ExecutorService threadPool = Executors.newFixedThreadPool(5, new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
                System.out.println(t.getName() + " 抛出了异常:" + e);
            }
        });
        return t;
    }
});

for (int i = 0; i < 5; i++) {
    threadPool.execute(() -> {
        System.out.println("current thread name:" + Thread.currentThread().getName());
        Object object = null;
        System.out.println("result:" + object.toString());
    });
}

打印如下:

current thread name:Thread-2
current thread name:Thread-4
current thread name:Thread-3
current thread name:Thread-5
current thread name:Thread-6
Thread-2 抛出了异常:java.lang.NullPointerException: Cannot invoke "Object.toString()" because "object" is null
Thread-4 抛出了异常:java.lang.NullPointerException: Cannot invoke "Object.toString()" because "object" is null
Thread-3 抛出了异常:java.lang.NullPointerException: Cannot invoke "Object.toString()" because "object" is null
Thread-5 抛出了异常:java.lang.NullPointerException: Cannot invoke "Object.toString()" because "object" is null
Thread-6 抛出了异常:java.lang.NullPointerException: Cannot invoke "Object.toString()" because "object" is null

第4种方式:重写ThreadPoolExecutor的afterExecute()方法。

在使用execute()提交任务的时候(submit()其实也是调用的execute()方法),会执行ThreadPoolExecutor的runWorker()方法,代码如下:

public class ThreadPoolExecutor extends AbstractExecutorService {

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted. This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                                runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x;
                        throw x;
                    } catch (Error x) {
                        thrown = x;
                        throw x;
                    } catch (Throwable x) {
                        thrown = x;
                        throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
}

上面的代码会不停地从任务队列中取出任务执行,如果run()方法抛出了异常,会把异常赋值给thrown,最终传给afterExecute()方法,这个方法默认是空的,我们可以自己实现这个方法进行处理:

class ExtendedExecutor extends ThreadPoolExecutor {
    // jdk文档里面的例子
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                Object result = ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }
        if (t != null)
            System.out.println(t);
    }
}