java 线程池知识点

122 阅读11分钟

github.com/ccnio/Wareh…

ThreadPoolExecutor

执行流程

image.png

public ThreadPoolExecutor(  
    int corePoolSize,  
    int maximumPoolSize,//必须  
    long keepAliveTime,//必须  
    TimeUnit unit,//必须  
    BlockingQueue<Runnable> workQueue,//必须  
    ThreadFactory threadFactory,  
    RejectedExecutionHandler handler)  
  • corePoolSize 核心线程数
  • maximumPoolSize 最大线程数,线程池允许创建的最大线程数。
  • workQueue 任务队列,BlockingQueue 接口的某个实现(常使用 ArrayBlockingQueue 和 LinkedBlockingQueue)。
  • keepAliveTime 空闲线程的保活时间,如果某线程的空闲时间超过这个值都没有任务给它做,那么可以被关闭了。注意这个值并不会对所有线程起作用,如果线程池中的线程数少于等于核心线程数 corePoolSize,那么这些线程不会因为空闲太长时间而被关闭。当然,也可以通过调用 allowCoreThreadTimeOut(true)使核心线程数内的线程也可以被回收,默认是false。
  • threadFactory 用于生成线程,一般我们可以用默认的就可以了。通常,我们可以通过它将我们的线程的名字设置得比较可读一些,如 Message-Thread-1, Message-Thread-2 类似这样。
  • handler:线程池的拒绝策略。jdk默认提供了四种拒绝策略,默认AbortPolicy :
  • CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,由调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
public static class CallerRunsPolicy implements RejectedExecutionHandler {  
    public CallerRunsPolicy() { }  
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
        if (!e.isShutdown()) {  
            r.run();  
        }  
    }  
}  
  • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
public static class AbortPolicy implements RejectedExecutionHandler {  
    public AbortPolicy() { }  
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
        throw new RejectedExecutionException();  
    }  
}  
  • DiscardPolicy - 直接丢弃,其他啥都没有
  • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

注意:如果将队列设置为无界队列,那么线程数达到 corePoolSize 后,其实线程数就不会再增长了。因为后面的任务直接往队列塞就行了,此时 maximumPoolSize 参数就没有什么意义。

源码解析

Worker 内部包含了一个 Thread,启动时调用本身实现的 runnable。 这里又用到了抽象类 AbstractQueuedSynchronizer 来处理并发问题。

private final class Worker  extends AbstractQueuedSynchronizer implements Runnable {  
    // 这个是真正的线程,任务靠你啦  
    final Thread thread;  
  
    //任务。为什么叫 firstTask?如果不为空,那么第一个任务就是存放在这里的  
    //也可以为 null,线程起来了自己到任务队列中取任务(getTask 方法)  
    Runnable firstTask;  
  
    // 用于存放此线程完成的任务数,注意了,这里用了 volatile,保证可见性  
    volatile long completedTasks;  
  
    // Worker 只有这一个构造方法,传入 firstTask,也可以传 null  
    Worker(Runnable firstTask) {  
        setState(-1); // inhibit interrupts until runWorker  
        this.firstTask = firstTask;  
        // 调用 ThreadFactory 来创建一个新的线程  
        this.thread = getThreadFactory().newThread(this);  
    }  
  
    // 这里调用了外部类的 runWorker 方法  
    public void run() {  
        runWorker(this);//开启此线程的工作  
    }  
  
    ...// 其他几个方法没什么好看的,就是用 AQS 操作,来获取这个线程的执行权,用了独占锁  
}  
  
// 此方法由 worker 线程启动后调用,这里用一个 while 循环来不断地从等待队列中获取任务并执行  
// 前面说了,worker 在初始化的时候,可以指定 firstTask,那么第一个任务也就可以不需要从队列中获取  
final void runWorker(Worker w) {  
    //   
    Thread wt = Thread.currentThread();  
    // 该线程的第一个任务(如果有的话)  
    Runnable task = w.firstTask;  
    w.firstTask = null;  
    w.unlock(); // allow interrupts  
    boolean completedAbruptly = true;  
    try {  
        // 循环调用 getTask 获取任务(getTask 源码见 QA)  
        while (task != null || (task = getTask()) != null) {  
            w.lock();            
            try {  
                // 这是一个钩子方法,留给需要的子类实现  
                beforeExecute(wt, task);  
                Throwable thrown = null;  
                try {  
                    // 到这里终于可以执行任务了  
                    task.run();  
                } finally {  
                    // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现  
                    afterExecute(task, thrown);  
                }  
            } finally {  
                // 置空 task,准备 getTask 获取下一个任务  
                task = null;  
                // 累加完成的任务数  
                w.completedTasks++;  
                // 释放掉 worker 的独占锁  
                w.unlock();  
            }  
        }  
        completedAbruptly = false;  
    } finally {  
        processWorkerExit(w, completedAbruptly);  
        //线程池中线程的回收依赖JVM自动的回收,  
        //线程池做的工作是根据当前线程池的状态维护一定数量的线程引用  
        //,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。  
    }  
}  
private Runnable getTask() {  
    boolean timedOut = false; // Did the last poll() time out?  
  
    for (;;) {  
        int c = ctl.get();  
        int rs = runStateOf(c);  
          
        int wc = workerCountOf(c);//线程数  
  
        // 判断当前线程数是否大于核心线程数,或核心线程是否允许被回收  
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;  
        try {  
            Runnable r = timed ?  
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //阻塞固定时间  
                workQueue.take();//一直阻塞  
            if (r != null) return r;  
            timedOut = true;  
        } catch (InterruptedException retry) {  
            timedOut = false;  
        }  
    }  
}  

任务的获取过程解答了下面两个问题:

  • 线程池是如何保证核心线程不被销毁的?
    线程池当未调用 shutdown 方法时,是通过BlockQuene的 take() 阻塞核心线程(Worker)的 run 方法从而保证核心线程不被销毁的,take注释如下:

Retrieves and removes the head of this queue, waiting if necessary until an element becomes available.

  • 线程池中线程是如何知道自己达到keepAliveTime时间,然后销毁的?

poll(long timeout, TimeUnit unit) 是一个非阻塞方法,如果当前队列为空,直接返回,注释如下:

Retrieves and removes the head of this queue, waiting up to the specified wait time if necessary for an element to become available.

 所以:我们的线程在获取任务时,如果队列中已经没有任务,会在此处阻塞keepALiveTime的时间,如果到时间都没有任务,就会 return null(不是直接返回null,是最终),然后在runWorker()方法中,执行processWorkerExit(w, completedAbruptly)终止线程

任务执行过程中发生异常怎么处理?
如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。

Executor / ExecutorService

ExecutorService 提供了一些常用的线程池配置:

//newFixedThreadPool  
public static ExecutorService newFixedThreadPool(int nThreads) {  
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,  
                    new LinkedBlockingQueue<Runnable>());  
}  
  
//newSingleThreadExecutor  
public static ExecutorService newSingleThreadExecutor() {  
    return new FinalizableDelegatedExecutorService(  
        new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,  
                  new LinkedBlockingQueue<Runnable>()));  
}  
  
//需要的时候就创建新的线程,同时可以复用之前创建的线程(如果这个线程当前没有任务)的线程池。  
public static ExecutorService newCachedThreadPool() {  
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,  
                 new SynchronousQueue<Runnable>());  
}  

这个接口中定义的一系列方法大部分情况下已经可以满足我们的需要了:

public interface ExecutorService extends Executor {  
    // 关闭线程池,已提交的任务继续执行,不接受继续提交新任务  
    void shutdown();  
      
    // 关闭线程池,尝试停止正在执行的所有任务,不接受继续提交新任务  
    // 它和前面的方法相比,加了一个单词“now”,区别在于它会去停止当前正在进行的任务  
    List<Runnable> shutdownNow();  
      
    // 线程池是否已关闭  
    boolean isShutdown();  
      
    // 如果调用了 shutdown() 或 shutdownNow() 方法后,所有任务结束了,那么返回true  
    // 这个方法必须在调用shutdown或shutdownNow方法之后调用才会返回true  
    boolean isTerminated();  
      
    // 等待所有任务完成,并设置超时时间  
    // 我们这么理解,实际应用中是,先调用 shutdown 或 shutdownNow,  
    // 然后再调这个方法等待所有的线程真正地完成,返回值意味着有没有超时  
    boolean awaitTermination(long timeout, TimeUnit unit)  
        throws InterruptedException;  
      
    // 提交一个 Callable 任务  
    <T> Future<T> submit(Callable<T> task);  
      
    // 提交一个 Runnable 任务,第二个参数将会放到 Future 中,作为返回值,  
    // 因为 Runnable 的 run 方法本身并不返回任何东西  
    <T> Future<T> submit(Runnable task, T result);  
      
    // 提交一个 Runnable 任务  
    Future<?> submit(Runnable task);  
      
    // 执行所有任务,返回 Future 类型的一个 list  
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)  
  
    // 也是执行所有任务,但是这里设置了超时时间  
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)  
  
    // 只有其中的一个任务结束了,就可以返回,返回执行完的那个任务的结果  
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)  
  
    // 同上一个方法,只有其中的一个任务结束了,就可以返回,返回执行完的那个任务的结果,  
    // 不过这个带超时,超过指定的时间,抛出 TimeoutException 异常  
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)  
}  

这些方法都很好理解,一个简单的线程池主要就是这些功能,能提交任务,能获取结果,能关闭线程池,这也是为什么我们经常用这个接口的原因。

FutureTask/Callable

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等,其实也是(volatile+CAS(Unsafe))

public interface Future<V> {  
    //mayInterruptIfRunning为true,则会立即中断执行任务的线程并返回true,为false,则会返回true且不会中断任务执行线程  
    boolean cancel(boolean mayInterruptIfRunning);  
    boolean isCancelled();  
    boolean isDone();  
    //获取任务执行结果,如果任务还没完成则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常  
    V get() throws InterruptedException, ExecutionException;  
    V get(long timeout, TimeUnit unit)  throws InterruptedException, ExecutionException, TimeoutException;  
}  

Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

<T> Future<T> submit(Runnable task, T result);  
Future<?> submit(Runnable task);  

下面这段代码展示两个任务同时执行,最多消耗 3000ms

public static void main(String[] args) throws ExecutionException, InterruptedException {  
    FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {  
        @Override  
        public String call() throws Exception {  
            Thread.sleep(1000);  
            return "futureTask 1000";  
        }  
    });  
      
    FutureTask<String> futureTask2 = new FutureTask<>(new Callable<String>() {  
        @Override  
        public String call() throws Exception {  
            Thread.sleep(3000);  
            return "futureTask 3000";  
        }  
    });  
    long startMills = System.currentTimeMillis();  
    new Thread(futureTask).start();  
    new Thread(futureTask2).start();  
    String result1 = futureTask.get(); //阻塞当前线程  
    String result2 = futureTask2.get(); //阻塞当前线程  
    System.out.println("consume " + (System.currentTimeMillis() - startMills));  
}  

BlockingQueue

线程池中的 BlockingQueue 也是非常重要的概念,其子类内部是通过ReentrantLock 来实现的,如果线程数达到 corePoolSize,我们的每个任务会提交到等待队列中,等待线程池中的线程来取任务并执行。java.util.concurrent包下具有以下 BlockingQueue 接口的实现类:

  • ArrayBlockingQueue:有界的阻塞队列,初始化的时候必须设定这个上限。其内部实现是将对象放到一个数组里。
  • LinkedBlockingQueue:链式结构(链接节点)对其元素进行存储。可以选择一个上限,如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • SynchronousQueue:是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。

如何合理配置线程池参数?

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

1. 任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
2. 任务的优先级:高,中和低。
3. 任务的执行时间:长,中和短。
4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU 密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。