线程池ThreadPoolExecutor

1、线程池的好处

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。比如:线程池、数据库连接池、http连接池等等都是对这个思想的应用。

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

2、Executor 框架

2.1、Executor 介绍

Executor 框架由 Java5 之后引入,通过 Executor 来启动线程比使用 Thread 的 start 方法更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题【this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发令人疑惑的错误】。
Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

2.2、Executor 框架(主要由三大部分组成)

2.2.1、任务( Runnable / Callable )

执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。

2.2.2、任务的执行(Executor)

如下图所示,包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。

这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

注意: 通过查看 ScheduledThreadPoolExecutor 源代码我们发现 ScheduledThreadPoolExecutor 实际上是继承了 ThreadPoolExecutor 并实现了 ScheduledExecutorService ,而 ScheduledExecutorService 又实现了 ExecutorService,正如我们下面给出的类关系图显示的一样。

ThreadPoolExecutor类继承结构

public interface Executor {...}
public interface ExecutorService extends Executor {...}
public abstract class AbstractExecutorService implements ExecutorService {...}
public class ThreadPoolExecutor extends AbstractExecutorService {...}

ScheduledThreadPoolExecutor类继承结构

public interface ScheduledExecutorService extends ExecutorService {...}
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {...}

2.2.3、异步计算的结果(Future)

Future 接口以及 Future 接口的实现类 FutureTask 类都可代表异步计算的结果。

当我们把 Runnable/Callable 接口的实现类提交给 ThreadPoolExecutorScheduledThreadPoolExecutorsubmit(...) 方法执行后会返回一个 FutureTask 对象。

public interface Future<V> {...}
public interface Runnable {...}
public interface RunnableFuture<V> extends Runnable, Future<V> {...}
public class FutureTask<V> implements RunnableFuture<V> {...}

2.3 Executor 框架的使用

  1. 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  2. 把创建完成的实现 Runnable/Callable 接口的对象直接交给 ExecutorService 的执行方法执行【
    public void execute(Runnable command);
    Future<?> submit(Runnable task);
    <T> Future<T> submit(Runnable task, T result);
    <T> Future<T> submit(Callable<T> task);】
  3. 如果执行 ExecutorService 类的 submit(…)方法,将返回一个实现 Future 接口的对象。
    由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
  4. 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。
    主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning) 来取消此任务的执行。

3、ThreadPoolExecutor实践

3.1 ThreadPoolExecutor 类解析

ThreadPoolExecutor 类中提供的四个构造方法。解析下最基本的构造函数。
在这里插入图片描述

// 用给定的初始参数创建一个新的ThreadPoolExecutor。
public ThreadPoolExecutor(
		int corePoolSize,//线程池的核心线程数量
        int maximumPoolSize,//线程池的最大线程数
        long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
        TimeUnit unit,//时间单位
        BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
        ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
        RejectedExecutionHandler handler)//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
{
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • ThreadPoolExecutor 3 个最重要的参数:
    corePoolSize :核心线程数线程数定义了最小可以同时运行的线程数量。
    maximumPoolSize :当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

  • ThreadPoolExecutor 另外4个参数:
    keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
    unit :keepAliveTime 参数的时间单位。
    threadFactory :executor 创建新线程的时候会用到。
    handler :饱和策略。

  • ThreadPoolExecutor 饱和策略定义:
    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
    ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
    ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
    ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

举个栗子:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略提供可伸缩队列。

3.2 推荐使用 ThreadPoolExecutor 构造函数创建线程池

在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。因为使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:
FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

3.3 与线程池相关的知识对比

3.3.1 Runnable 对比 Callable

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理 Runnable 接口不会返回结果或抛出检查异常的缺陷,但是Callable 接口可以。所以如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

3.3.2 execute() 对比 submit()

execute(...) 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
submit(...) 方法用于提交需要返回值的任务。可以通过线程池会返回值 Future 对象判断任务是否执行成功 ,并且可以通过 Futureget() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

1. AbstractExecutorService 接口中的一个 submit() 方法的源代码:
public Future<?> submit(Runnable task) {
	if (task == null) throw new NullPointerException();
	RunnableFuture<Void> ftask = newTaskFor(task, null);
	execute(ftask);
	return ftask;
}

2. 上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

3. 我们再来看看execute()方法:
public void execute(Runnable command) {
  ...
}

3.3.3 shutdown() 对比 shutdownNow()

shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

3.3.2 isTerminated() 对比 isShutdown()

isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

4、jdk提供的几种常见线程池

4.1 Executors.newFixedThreadPool(…)

Executors.newFixedThreadPool(…) 被称为可重用固定线程数的线程池。

=== 创建一个可重用固定数量线程的线程池:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

=== 另外还有一个 FixedThreadPool 的实现方法,和上面的类似,所以这里不多做阐述:
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

从源代码可以看出新创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。

4.1.1 执行任务过程介绍

FixedThreadPool 的 execute() 方法运行示意图(该图片来源:《Java 并发编程的艺术》):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ivs8Rwb-1622702398656)(/images/java线程池学习总结/FixedThreadPool.png)]

上图说明:

如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

4.1.2 为什么不推荐使用FixedThreadPool?

FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :

当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

4.2 Executors.newSingleThreadExecutor(…)

Executors.newSingleThreadExecutor(...) 是只有一个线程的线程池。下面看看SingleThreadExecutor 的实现:

=== 返回只有一个线程的线程池
方法一:
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
方法二:
public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService
	    (new ThreadPoolExecutor(1, 1,
	                            0L, TimeUnit.MILLISECONDS,
	                            new LinkedBlockingQueue<Runnable>()));
}

4.2.1 执行任务过程介绍

SingleThreadExecutor 的运行示意图(该图片来源:《Java 并发编程的艺术》):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2myW5kHD-1622702398656)(/images/java线程池学习总结/SingleThreadExecutor.png)]

上图说明;

如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;

4.2.2 为什么不推荐使用SingleThreadExecutor?

SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM。

4.3 Executors.newCachedThreadPool(…)

Executors.newCachedThreadPool(...) 是一个会根据需要创建新线程的线程池。

创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
方法一:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

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

4.3.1 执行任务过程介绍

CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NDbzlpq2-1622702398657)(/images/java线程池学习总结/CachedThreadPool-execute.png)]

上图说明:

首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

4.3.2 为什么不推荐使用CachedThreadPool?

CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

5、ScheduledThreadPoolExecutor实践

6、线程池大小确定

线程池数量设置的过少则会增长做事的时间,从而降低做事效率。线程池数量设置的过多会产生过多的上下文切换,也会降低做事效率。线程池大小设置过大或者过小都会有问题,合适的才是最好。

上下文切换:
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的方式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换
上下文切换通常是计算密集型的。它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。


如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。


有一个简单并且适用面比较广的公式:

  1. CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  2. I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值