Java 线程池详解

基本概念

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3 个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

在 Java 中代表线程池的类有以下主要的三个:

  • ThreadPoolExecutor
  • ForkJoinPool
  • ScheduledThreadPoolExecutor

其中最常用的是 ThreadPoolExecutor,以下就主要谈谈 ThreadPoolExecutor 的特点和使用方法。

构造函数

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

ThreadPoolExecutor 有四个构造函数,其中上面的是最全也是参数做多的一个构造函数。一共有七个参数。

  • corePoolSize - 线程池中核心线程的数量
  • maximumPoolSize - 线程池中允许的最大线程数量
  • keepAliveTime - 当线程池中的线程数量大于corePoolSize 时,空闲时间超过 keepAliveTime 的线程就会自动结束。allowCoreThreadTimeOut(boolean) 可以设置是否核心线程也会收到影响。
  • unit - keepAliveTime 的单位
  • workQueue - 存储任务的工作队列。
  • threadFactory - 线程池创建线程时使用的工厂。
  • handler - 执行拒绝策略的具体类。

corePoolSize & maximumPoolSize

ThreadPoolExecutor 会根据这两个参数自动调整线程池中线程的数量。当一个任务被提交到线程池的时候:

  1. 线程数量在 [0,corePoolSize ) 之间:无论线程池中是否有空闲的线程都会创建一个线程来执行任务。
  2. 线程数量 [corePoolSize ,maximumPoolSize) 之间:当且仅当工作队列满的时候才创建新的线程。
  3. 线程数量 [maximumPoolSize,∞) 之间:执行拒绝策略, RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)

keepAliveTime & unit

如果线程池中有多于 corePoolSize 的线程数量,这些多余的线程就会被结束如果它们空闲的时间超过 keepAliveTime。这可以在线程池不是很活跃的时候有效的减少资源消耗。默认情况下这个超时策略只会应用在多于核心线程数的那部分线程上,但是可以使用 allowCoreThreadTimeOut(boolean) 来让核心线程也有超时策略,前提是 keepAliveTime 不为 0。

unitkeepAliveTime 的单位。

workQueue

存储任务的工作队列,是一个阻塞队列。当一个任务被提交到线程池的时候,可能要进入工作队列进行排队,以下是三种常见的排队策略:

  • Direct handoffs - 可以使用 SynchronousQueue 来实现这样的线程池。在这种策略下,线程池不会保存要执行的任务,也就是被提交的任务会立刻被执行。这一策略往往需要 maximumPoolSize 是无限大的 (Integer.MAX_VALUE)
  • Unbounded queues - 使用无界队列 (LinkedBlockingQueue) 实现的线程池。在这种策略下,不会有多于 corePoolSize 的线程,因此 maximumPoolSize 其实是不起作用的。但是当提交任务的速度大于任务执行的速度,资源占用会爆炸。
  • Bounded queues - 使用有界队列 ( ArrayBlockingQueue) 实现的线程池。此策略可以有效防止资源被耗尽,但是在线程池的调整和控制上就会略显困难。

当传给构造函数的队列中已经有任务的时候,可以调用 prestartCoreThread() or prestartAllCoreThreads() 两个方法来创建核心线程,不需要等到下一次提交任务。

threadFactory

创建线程的一个工厂类,在线程池需要创建线程的时候就会调用这个方法。

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

handler

在线程池达到饱和的时候,就会执行拒绝策略,实际上就是调用 RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)方法。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

ThreadPoolExecutor 类中已经提前定义了四种拒绝策略。

ThreadPoolExecutor.AbortPolicy

总是抛出 RejectedExecutionException 异常。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}
ThreadPoolExecutor.CallerRunsPolicy

由调用这个方法的线程来执行。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}
ThreadPoolExecutor.DiscardPolicy

啥也不干,直接丢弃任务。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
ThreadPoolExecutor.DiscardOldestPolicy

把最先提交的任务给丢掉,然后把当前的任务再提交到线程池中。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}

线程池参数

线程池的最重要的两个控制参数是 runStateworkerCount,它们存储在一个原子整型变量中。其中 runState 占高 3 位,剩下的 29 位是 workerCount

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

runState 有五个值,分别是:

  • RUNNING: 接收提交的任务并且执行工作队列中的任务。
  • SHUTDOWN: 不接收提交的任务,但执行工作队列中的任务。
  • STOP: 不接收提交的任务,也不执行工作队列中的任务,并且中断正在执行的任务。
  • TIDYING: 所有的任务都已经结束,workerCount 为0,调用钩子函数 terminated()
  • TERMINATED: terminated() 方法执行结束。

一般来讲,线程池的状态变化都是从小到大的,但是并不需要经历每一个状态。

状态变化方式
RUNNING -> SHUTDOWN调用 shutdown() 方法,或者隐式的调用 finalize()
(RUNNING or SHUTDOWN) -> STOP调用 shutdownNow() 方法
SHUTDOWN -> TIDYING工作队列为空,线程数量为 0
STOP -> TIDYING工作队列为空
TIDYING -> TERMINATEDterminated() 方法执行结束。

任务提交的流程

When a new task is submitted in method execute(Runnable), and fewer than corePoolSize threads are running, a new thread is created to handle the request, even if other worker threads are idle. If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full.

当向线程池提交一个任务之后,它的执行流程如下图所示:

ThreadPoolExecutor 执行 execute() 方法分下面4种情况:

  1. 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步骤
    需要获取全局锁)。
  2. 如果运行的线程大等于 corePoolSize,则将任务加入 BlockingQueue
  3. 如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执
    行这一步骤需要获取全局锁)。
  4. 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并调用
    RejectedExecutionHandler.rejectedExecution() 方法。

ThreadPoolExecutorexecute() 方法源代码如下所示:

/**
 * Executes the given task sometime in the future.  The task
 * may execute in a new thread or in an existing pooled thread.
 *
 * If the task cannot be submitted for execution, either because this
 * executor has been shutdown or because its capacity has been reached,
 * the task is handled by the current {@code RejectedExecutionHandler}.
 *
 * @param command the task to execute
 * @throws RejectedExecutionException at discretion of
 *         {@code RejectedExecutionHandler}, if the task
 *         cannot be accepted for execution
 * @throws NullPointerException if {@code command} is null
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.		
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    
    // ctl存储着工作线程数和线程池状态,同过workerCountOf()和runStateOf()分别获取
    int c = ctl.get();
    // 工作线程数小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 向线程池中提交任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    
    // 至此,要么工作线程数大于等于核心线程数,或者提交任务失败
    // 如果线程池处于运行状态,向工作队列中插入一个任务
    if (isRunning(c) && workQueue.offer(command)) {
        // 需要进行二次检查来确定是否需要创建一个线程,或者防止线程池已经关闭
        int recheck = ctl.get();
        // 线程池已经关闭
        if (! isRunning(recheck) && remove(command))
            // 执行拒绝策略
            reject(command);
        // 需要创建一个新的线程
        else if (workerCountOf(recheck) == 0)
            // 开启一个新线程,防止提交任务后正好所有的线程的结束了
            addWorker(null, false);
    }
    // 工作队列已满
    // 提交任务失败表示线程池已经饱和或者已经关闭
    else if (!addWorker(command, false))
        // 执行拒绝策略
        reject(command);
}

Executors工厂创建线程池

由于 TheadPoolExecutor 的构造函数参数众多,因此不同的组合可以产生不同功能和性能的线池。Java 的 Executors 类中提供了几个工厂方法用于创建线程池。
newCachedThreadPool() 返回的线程池能在有需要的时候创建线程,对最大线程数量不做限制,核心线程数量为 0 ,线程空闲时间超过 60 秒就会被结束。属于上面所说到的 Direct handoffs 类型的排队类型。在任务数量大的时候会为每一个任务创建一个线程来执行,当任务数量减少的时候多余的线程又会因为超时策略被结束。非常适合用于执行耗时短的异步任务。

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

newFixedThreadPool(int nThreads) 返回的线程池最多能创建 nThreads 个线程,并且这些线程都是核心线程。随着任务数量的增加,线程池中的线程也会增加直到 nThreads 。如果有线程因为异常结束,也会新建一个线程来顶替。这种线程池无论提交的任务的数量是多少,线程的数量最多不会超过 nThreads 个,可以减少资源的占用。但是当任务的提交速度大于执行速度后,由于队列是无限的,会占用过多的资源。

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

newScheduledThreadPool(int corePoolSize) 返回一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

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

newSingleThreadExecutor() 返回一个只有一个线程的线程池。此类线程池可以保证提交任务的执行顺序。

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

newSingleThreadScheduledExecutor() 返回只有一个线程的支持定时以及周期性执行任务的线程池。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}

newWorkStealingPool() 返回一个可进行工作窃取的 ForkJoinPool 线程池。

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

execute()和submit()

一般往线程池中提交任务使用 execute()submit() 两个方法。使用 execute() 不会返回任何值;使用 submit() 提交之后返回一个 FutureTask 类,调用它的 get() 方法能够阻塞执行任务的线程直到取到执行结果。

钩子函数

为了支持扩展,TheadPoolExecutor 提供了下面几个钩子函数。通过继承可以重载这些函数实现功能扩展。

/* Extension hooks */

// 任务执行前调用
protected void beforeExecute(Thread t, Runnable r) { }

// 任务执行后调用
protected void afterExecute(Runnable r, Throwable t) { }

// 线程池关闭后调用
protected void terminated() { }

线程池的关闭

要关闭一个线程池,可以使用 shutdown()shutdownNow() 方法。两个方法最终都能关闭线程池,但是还一样一些区别。

shutdown():先将线程池的状态设置为 SHUTDOWN,然后关闭所有的空闲的线程。

shutdownNow():先将线程池的状态设置为 STOP,然后关闭所有的工作线程,并返回工作队列中还没有被执行过的任务。

总的来说,shutdown() 会让线程池中已经提交的任务都执行完成;shutdownNow() 会停止所有正在执行的任务,返回没有被执行过的任务的集合。

使用实例

这里使用了官方的例子,扩展了 ThreadPoolExecutor,让它具有暂停执行任务的功能。

/**
 * @author XinHui Chen
 * @date 2020/2/10 19:36
 */
public class ExecutorDemo {
    public static void main(String[] args) {
        PausableThreadPoolExecutor pausableThreadPoolExecutor = new PausableThreadPoolExecutor(5, // 核心线程数为5
                                                                                                                    10, // 最大线程数为10
                                                                                                                    60, //keepAliveTIme 为60秒
                           TimeUnit.SECONDS, 
                                                                                                                    new LinkedBlockingQueue<Runnable>(7), // 阻塞队列的大小为 7
                                                                                                                    Executors.defaultThreadFactory(), // 使用默认线程工厂
                                                                                         
                           (r, executor) -> System.out.println("task discard")); //丢弃任务并打印信息
		
        // 暂停线程池运行
        pausableThreadPoolExecutor.pause();
        
        // 提交20个任务
        for (int i = 0; i < 20; i++) {
            pausableThreadPoolExecutor.execute(new TimePrinter());
        }
       
        // 恢复运行
        pausableThreadPoolExecutor.resume();
        // 关闭线程池
        pausableThreadPoolExecutor.shutdown();
    }

}

class TimePrinter implements Runnable {

    @Override
    public void run() {
        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(Thread.currentThread()
                .getName() + " " + localDateTime.toString());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class PausableThreadPoolExecutor extends ThreadPoolExecutor {
    private boolean isPaused;

    private ReentrantLock pauseLock = new ReentrantLock();

    private Condition unpaused = pauseLock.newCondition();

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

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        pauseLock.lock();
        try {
            while (isPaused) {
                unpaused.await();
            }
        } catch (InterruptedException ie) {
            t.interrupt();
        } finally {
            pauseLock.unlock();
        }
    }

    public void pause() {
        pauseLock.lock();
        System.out.println("Pause");
        try {
            isPaused = true;
        } finally {
            pauseLock.unlock();
        }
    }

    public void resume() {
        pauseLock.lock();
        System.out.println("Resume");
        try {
            isPaused = false;
            unpaused.signalAll();
        } finally {
            pauseLock.unlock();
        }
    }
}

执行结果如下所示。首先让线程池暂停执行任务,之后向里面提交20个任务,然后在让它恢复执行。由于线程池的 corePoolSize 设置为 5, maximumPoolSize 设置为 10,工作队列的容量为 7,所以能容纳的最多任务数量是17个任务,多余的三个任务就被拒绝。

在调用 resume() 方法之后,线程池开始工作,会有是个线程同时执行任务。完成之后其中的七个线程继续执行剩下的七个任务。

Pause
task discard
task discard
task discard
Resume
pool-1-thread-2 2020-02-11T20:48:19.560
pool-1-thread-1 2020-02-11T20:48:19.560
pool-1-thread-5 2020-02-11T20:48:19.560
pool-1-thread-4 2020-02-11T20:48:19.560
pool-1-thread-7 2020-02-11T20:48:19.560
pool-1-thread-6 2020-02-11T20:48:19.560
pool-1-thread-10 2020-02-11T20:48:19.560
pool-1-thread-8 2020-02-11T20:48:19.560
pool-1-thread-3 2020-02-11T20:48:19.560
pool-1-thread-9 2020-02-11T20:48:19.560
pool-1-thread-2 2020-02-11T20:48:20.561
pool-1-thread-1 2020-02-11T20:48:20.561
pool-1-thread-5 2020-02-11T20:48:20.561
pool-1-thread-4 2020-02-11T20:48:20.561
pool-1-thread-7 2020-02-11T20:48:20.561
pool-1-thread-6 2020-02-11T20:48:20.561
pool-1-thread-10 2020-02-11T20:48:20.561
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值