Java 线程池详解(二): Executors

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是 ExecutorService。Executors 作用主要用来创建线程池,相当于一个线程工厂,但是在阿里巴巴的《Java开发手册》中明确指出:

虽然不推荐使用,但是可以了解一下 Executors ,方便理解线程池。

Executors 可以创建六中以下线程池:

  • FixedThreadPool(n):创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。
  • CachedThreadPool():短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。
  • SingleThreadExecutor():创建一个单线程线程池。
  • ScheduledThreadPool(n):创建一个数量固定的线程池,支持执行定时性或周期性任务。
  • SingleThreadScheduledExecutor():此线程池就是单线程的 newScheduledThreadPool。
  • WorkStealingPool(n):Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

下面分别来看以上六种线程池的具体代码使用:

FixedThreadPool 使用

创建固定个数的线程池。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
    fixedThreadPool.execute(() -> {
        System.out.println("CurrentTime - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行结果如下:

CurrentTime - 2020-01-19 08:57:40
CurrentTime - 2020-01-19 08:57:40
CurrentTime - 2020-01-19 08:57:41

newFixedThreadPool(2)  创建了两个线程,将前两个线程放入线程池中并执行,第三个线程进行等待,当前面两个其中的某一个执行完后,有了空闲线程,第三个线程就会执行。

CachedThreadPool 使用

根据实际需要自动创建带缓存功能的线程池。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    cachedThreadPool.execute(() -> {
    System.out.println("Current Time - " +   LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

newCachedThreadPool 在短时间内会创建多个线程来处理对应的任务,并试图把它们进行缓存以便重复使用。

SingleThreadExecutor 使用

创建单个线程的线程池,具体代码如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 3; i++) {
    singleThreadExecutor.execute(() -> {
        System.out.println("CurrentTime - " +           LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

newSingleThreadExecutor() 会创建一个线程池(这个线程池只有一个线程),这个线程池可以在线程结束后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。

ScheduledThreadPool 使用

创建一个可以执行周期性任务的线程池。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
scheduledThreadPool.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());

输出结果如下:

2020-01-22T19:36:49.973
ThreadPool:2020-01-22T19:36:50.931

根据执行结果可以看出,我们设置的 1 秒后执行的任务生效了。

SingleThreadScheduledExecutor 使用

创建一个可以执行周期性任务的单线程池。

ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.schedule(() -> {
    System.out.println("ThreadPool:" + LocalDateTime.now());
}, 1L, TimeUnit.SECONDS);
System.out.println("CurrentTime:" + LocalDateTime.now());

WorkStealingPool 使用

Java 8 新增的创建线程池的方式:工作窃取线程池,其可根据当前电脑 CPU 处理器内核数量生成相应个数的线程池,其非常适合使用在很耗时的操作。但是 newWorkStealingPool 不是 ThreadPoolExecutor 的扩展,它是新的线程池类 ForkJoinPool 的扩展,但是都可以使用 Executors 类来创建。

int cores = Runtime.getRuntime().availableProcessors();
ExecutorService workStealingPool = Executors.newWorkStealingPool();
workStealingPool.execute(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Thread1: " + Thread.currentThread().getName() + " - " + LocalDateTime.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
for (int i = 0; i < cores; i++) {
    workStealingPool.execute(() -> {
        try {
            TimeUnit.SECONDS.sleep(4);
            System.out.println("Thread2: " + Thread.currentThread().getName() + " - " + LocalDateTime.now());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
// 因为 workStealingPool 是 Deamon 线程,即后台线程,精灵线程,守护线程;所以当main方法结束时, 此方法虽然还在后台运行,但是无输出,这时就可以通过对主线程阻塞来进行输出
System.in.read();

以上输出结果如下:

Thread1: ForkJoinPool-1-worker-1 - 2020-01-23T16:25:43.386
Thread2: ForkJoinPool-1-worker-3 - 2020-01-23T16:25:46.331
Thread2: ForkJoinPool-1-worker-0 - 2020-01-23T16:25:46.331
Thread2: ForkJoinPool-1-worker-2 - 2020-01-23T16:25:46.331
Thread2: ForkJoinPool-1-worker-1 - 2020-01-23T16:25:47.387

newWorkStealingPool 是并行处理任务的,并不能保证执行顺序。因为我的电脑是 4 核,所以 newWorkStealingPool 会创建只能容纳 4 个线程的线程池,当第一个 Thread1 添加到线程池后,又添加了 4 个线程,但是最后的一个线程会等待,只有前 4 个线程会执行;第一个线程延时 1s 先输出,然后继续输出后 3 个线程;当 1s 的输出完之后,会窃取最后一个等待的线程并执行;从上面输出结果的时间中就可以体现出这一点。

总结

Executors 可以创建 6 种不同类型的线程池,其中 newFixedThreadPool() 适合执行单位时间内固定的任务数,newCachedThreadPool() 适合短时间内处理大量任务,newSingleThreadExecutor() 和 newSingleThreadScheduledExecutor() 为单线程线程池,而 newSingleThreadScheduledExecutor() 可以执行周期性的任务,是 newScheduledThreadPool(n) 的单线程版本,而 newWorkStealingPool() 为 JDK 8 新增的并发线程池,可以根据当前电脑的 CPU 处理数量生成对比数量的线程池,但它的执行为并发执行不能保证任务的执行顺序。

文章部分内容参考《老王的 GitChat 专栏》:https://gitbook.cn/gitchat/column/5d493b4dcb702a087ef935d9/topic/5d4e13ae69004b174cd00140

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注