文章5:线程池的使用

80 阅读6分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第5篇文章,点击查看活动详情

为什么要使用线程池

多线程使用会出现的两个问题:

  1. 多个线程会占用过多内存
  2. 反复创建线程开销过大

解决这两个问题的方法:

  1. 使用尽量少线程,不让太多线程占用内存。
  2. 让这部分线程都保持工作,可以反复执行任务。避免线程生命周期的损耗。

线程池最核心的部分:让线程可以反复执行任务

大概演示线程池最重要的线程复用的方式,先有个印象

Runnable task = Queue.pop();

while (task != null) {
    task.run();
    task = Queue.pop();
}

如何创建线程池

手动创建

// 核心线程数
private static final int CORE_POOL_SIZE = 10;

// 最大线程数
private static final int MAXIMUM_POOL_SIZE = 10;

// 空闲线程存活时间、单位。非核心线程空闲时间超过此参数将会被回收
private static final long KEEP_ALIVE_TIME = 60;
private static final TimeUnit UNIT = TimeUnit.SECONDS;

// 线程池所使用的缓冲队列
private static final BlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<>(10);

// 线程池创建线程使用的工厂
private static final ThreadFactory THREAD_FACTORY = Thread::new;

// 线程池拒绝处理任务时的拒绝策略
private static final RejectedExecutionHandler HANDLER = (Runnable r, ThreadPoolExecutor e) -> {
    throw new RejectedExecutionException("Task " + r.toString() +
            " rejected from " +
            e.toString());
};

public static void main(String[] args) {
    ThreadPoolExecutor executorService = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE_TIME,
            UNIT,
            WORK_QUEUE,
            THREAD_FACTORY,
            HANDLER
    );
}

这里也感叹一下线程池的封装,将所有的操作参数全部暴露出来给到调用者去设置,最大程度的将线程池的内部逻辑与用户操作部分解耦。真是好设计啊,值得学习。

能考虑的这么全面,要是我来设计的话估计就只能是暴露个 CORE_POOL_SIZE出来就差不多了。

线程池添加线程规则:

  1. 如果线程数小于 CORE_POOL_SIZE,即使其他工作线程处于空闲状态,也会创建一个新的线程来运行新任务。
  2. 如果线程数等于(或大于)CORE_POOL_SIZE 但少于 MAXIMUM_POOL_SIZE,则将任务放入队列。
  3. 如果队列已满,并且线程数小于 MAXIMUM_POOL_SIZE,则创建一个新线程来运行任务。
  4. 如果队列已满,并且线程数大于或等于 MAXIMUM_POOL_SIZE,则拒绝该任务。

线程池添加线程规则.png

自动创建

线程池的自动创建是利用 Executors 工具类中的静态方法来创建线程池对象。Executors 为我们定义了一些比较有特性的线程池。如下是四个常用的线程池:

FixedThreadPool:定长线程池

可控制线程最大并发数,超出的线程会在队列中等待。

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

CachedThreadPool:可缓存线程池

如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

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

SingleThreaded:单线程的线程池

只会用唯一的工作线程来执行任务。

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

ScheduledThreadPool:支持定时及周期性任务执行

可以当做定时任务使用。

// 创建
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

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

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue());
}

// 延迟 5s 执行
scheduledExecutorService.schedule(() -> {
    // run 方法
}, 5, TimeUnit.SECONDS);

// 延迟 1s 执行,之后每隔 3s 执行一次
scheduledExecutorService.scheduleAtFixedRate(() -> {
    // run 方法
}, 1, 3, TimeUnit.SECONDS);

以上四种线程构造函数参数对比:

ParamterFixedThreadPoolCachedThreadPoolSingleThreadedScheduledThreadPool
corePoolSizeconstructor-arg01constructor-arg
maxPoolSizesame as corePoolSizeInteger.MAX_VALUE1Integer.MAX_VALUE
keepAliveTime0 seconds60 seconds0 seconds0 seconds

如何停止线程池

停止线程相关的 5 个方法:

void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTerminated(long timeout, TimeUnit unit) throws InterruptedException;
List<Runnable> shutdownNow();

shutdown();

调用 shutdown 方法之后,线程池并不是立刻就被关闭。事实上这个方法仅仅是初始化整个关闭过程,因为这个时候线程池中可能还有很多任务正在被执行,或者是任务队列中有大量正在等待被执行的任务,所以不是调用 shutdown 方法就立即关闭。在执行这个方法之后,线程池就接收到关闭信息,所以这个时候线程池为了优雅起见,会把正在执行的任务以及队列中等待的任务都执行完毕之后再关闭。

isShutdown();

isShutdown() 方法可以返回一个布尔值,true 或者 false 来判断线程池是不是已经开始关闭工作,也就是是否执行了 shutdown() 或者 shutdownNow() 方法。这个停止不是说完全停止,因为完全停止指的是所有的任务都执行完毕。

isTerminated();

返回整个线程池是不是已经完全终止了,这不仅仅线程池已经关闭,同时代表线程池中的所有任务都执行完毕了,就是线程池里面的线程包括正在执行的任务以及队列里面的任务都执行完了。

awaitTerminated(long timeout, TimeUnit unit);

这个方法作用相对比较弱,它不是用来停止线程池的,而是用来判断线程池的状态的。比如我们给 awaitTermination() 方法传入的参数是 10 秒,那么它就会等待 10 秒钟。

调用 awaitTermination() 方法之后,当前线程会等待一段时间,如果在等待的这段时间内,线程池已经关闭并且内部任务都执行完毕了,这个方法会返回 true,否则超时会返回 false。

所以这个方法只是一个用来测试在一段时间内这个线程是不是完全停止的。它起到的主要作用是检测,而不是关闭。

shutdownNow();

这个方法比较暴力,它与前面我们介绍的方法都不一样,这个方法后面带了一个 Now,也就表示立刻关闭的意思。如果要想立刻关闭掉,我们作为线程池的设计者,我们想一下应该怎么办才比较优雅。在执行 shutdownNow() 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务List来进行一些补救的操作,例如记录在案并在后期重试。

线程池的钩子函数

public class MyThreadPool extends ThreadPoolExecutor {

    // 此处省略构造函数

    // 在每个线程执行之前调用
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        System.out.println(t.getName() + "开始执行");
    }

    // 在每个线程执行之后调用
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }

    // 在执行 shutdown 方法之后,并且线程池中所有线程执行结束时调用
    @Override
    protected void terminated() {
        System.out.println("所有线程执行完毕");
    }

    public static void main(String[] args) {

        // 此处省略创建 threadPool 对象代码

        threadPool.execute(() -> {
            System.out.println("我被执行了。。。");
        });

        threadPool.shutdown();
    }
}