Java实习生面试复习(十):线程池ThreadPoolExecutor学习

导读:本篇文章讲解 Java实习生面试复习(十):线程池ThreadPoolExecutor学习,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

我是一名很普通的大三学生。我会坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,奥利给!!

如果你觉得内容对你有帮助的话,不如给个赞鼓励一下更新😂?(σ゚∀゚)σ…:*☆哎哟不错哦

生活从来都是公平的,你未来的模样,藏在你现在的努力里。

线程池

线程池是什么,好处是啥?

线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。
通过线程池复用线程有以下几点优点:

  • 减少资源创建 => 减少内存开销,创建线程占用内存
  • 降低系统开销 => 创建线程需要时间,会延迟处理的请求
  • 提高稳定稳定性 => 避免无限创建线程引起的OutOfMemoryError【简称OOM】

在阿里巴巴的《Java 开发手册》中是这样规定线程池的

  • 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
  • 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:Executors返回的线程池对象的弊端如下:
    1) FixedThreadPool和SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
    2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

Executors创建线程池,根据返回的对象类型创建线程池有三种:

  • 创建返回ThreadPoolExecutor对象
  • 创建返回ScheduleThreadPoolExecutor对象
  • 创建返回ForkJoinPool对象

这里只讨论创建返回ThreadPoolExecutor对象,其他两种我也不太懂,就不乱写了🤐,hh。

ThreadPoolExecutor详解

在聊Executors 之前,我们必须要先学习一下ThreadPoolExecutor,才知道为什么会提到线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,我们就从它的核心参数聊起吧。

ThreadPoolExecutor 的核心参数指的是它在构建时需要传递的参数,其构造方法如下所示:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // maximumPoolSize 必须大于 0,且必须大于 corePoolSize
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值
  • 对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。
  • 而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过 多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。
  • maximumPoolSize 表示线程池最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务队列满时,才会用到,一个线程池最大承载量等于 maximumPoolSize + workQueue的容量

  • keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。

  • unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。

  • workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。

  • threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程(🤣我也没设过,都用的默认的)。

  • handler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。

部分参数详解

比如针对面试题:线程池都有哪几种工作队列?或者也叫workQueue 都有哪几种工作队列?

  • ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
  • SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列

比如针对面试题:线程池的几种拒绝策略

  • AbortPolicy(直接抛出异常,也是默认的
  • DiscardPolicy(抛弃处理不了的任务,允许任务丢失
  • DiscardOldestPolicy(抛弃队列中等待的最久的任务
  • CallerRunsPolicy(将处理不了的回退给调用者,也可以理解为交给线程池调用所在的线程进行处理

如果我们要自定义拒绝策略,那么只需要新建一个 RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可

线程池是怎么判断执行拒绝策略的?我们可以看 execute() ,源码如下:

    public void execute(Runnable command) {
    	// ...省略其他代码
    	// 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
		(!addWorker(command, false))
			// 执行拒绝策略
            reject(command);
    }

知识扩展 execute() VS submit()
execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而
execute() 不能接收返回值。

使用案例:

public class ThreadPoolWriteDemo {
    public static void main(String[] args) {
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                2,  // 线程池中的常驻核心线程数
                5,  // 线程池中执行的最大线程数,它包含前者
                2L, // 多余的空闲线程存活时间
                TimeUnit.SECONDS,   // 时间的单位
                new LinkedBlockingQueue<>(3),   // 等待任务队列,即被提交单尚未被执行的任务
                Executors.defaultThreadFactory(),   // 生成线程池中工作线程的线程工厂,一般默认即可
                // 直接抛出异常
                new ThreadPoolExecutor.AbortPolicy()    // 拒绝策略
        );
        try {
            for (int i = 0; i < 10; i++) {
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPoolExecutor1.shutdown();
        }
    }
}

了解一下Executors

上面提到线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,其实当我们去看 Executors 的源码会发现,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底层都是通过ThreadPoolExecutor 实现的。

   /**
    * FixedThreadPool是固定核心线程的线程池,固定核心线程数由用户传入
    *
    * corePoolSize => nThreads,核心线程池的数量为1
    * maximumPoolSize => 等于corePoolSize核心线程数
    * keepAliveTime => 0L
    * unit => 毫秒
    * workQueue => LinkedBlockingQueue
    * 它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常
    */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
   /**
    * SingleThreadExecutor是单线程线程池,只有一个核心线程
    * corePoolSize => 1,核心线程池的数量为1
    * maximumPoolSize => 1,线程池最大数量为1,即最多只可以创建一个线程,唯一的线程就是核心线程
    * keepAliveTime => 0L
    * unit => 毫秒
    * workQueue => LinkedBlockingQueue
    */    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
   /**
    * CachedThreadPool是一个根据需要创建新线程的线程池
    *
    * corePoolSize => 0,核心线程池的数量为0
    * maximumPoolSize => Integer.MAX_VALUE,线程池最大数量为Integer.MAX_VALUE,可以认为可以无限创建线程
    * keepAliveTime => 60L
    * unit => 秒
    * workQueue => SynchronousQueue 一个不存储元素的阻塞队列
    *
    */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

    // 一池5个受理线程,类似一个银行有5个受理窗口
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    // 一池1个工作线程,类似一个银行有1个受理窗口
    ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
    // 一池N个工作线程,类似一个银行有N个受理窗口
    ExecutorService threadPool2 = Executors.newCachedThreadPool();
    
    try {
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t办理业务");
            });
            TimeUnit.SECONDS.sleep(1);
        }
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        threadPool.shutdown();
    }

从上述中我们可以看出Executors创建返回ThreadPoolExecutor对象的方法共有三种:

  • Executors#newCachedThreadPool => 创建一个根据需要创建新线程的线程池
  • Executors#newSingleThreadExecutor => 创建单线程的线程池
  • Executors#newFixedThreadPool => 创建固定核心线程的线程池

怎么终止线程池

	// 关闭,不会接受新的任务,正在执行的任务会继续执行下去,没有被执行的则中断。
    public void shutdown() {}

	// 正在执行的任务则被停止,没被执行任务的则返回。
    public List<Runnable> shutdownNow() {}
    
	// executor 是否已经关闭了,返回值 true 表示已关闭
    public boolean isShutdown() {}

线程池shutdown与shutdownNow有什么区别

  • shutdown会把线程池的状态改为SHUTDOWN,而shutdownNow把当前线程池状态改为STOP
  • shutdown只会中断所有空闲的线程,而shutdownNow会中断所有的线程。
  • shutdown返回方法为空,会将当前任务队列中的所有任务执行完毕;而shutdownNow把任务队列中的所有任务都取出来返回。

小结

线程池的使用必须要通过 ThreadPoolExecutor 的方式来创建,这样才可以更加明确线程池的运行规则,规避资源耗尽的风险。同时,也介绍了 ThreadPoolExecutor 的七大核心参数,比如核心线程数和最大线程数之间的区别,当线程池的任务队列没有可用空间且线程池的线程数量已经达到了最大线程数时,则会执行拒绝策略(4 种),用户也可以通过重写 rejectedExecution() 来自定义拒绝策略。

我们下期再见🙈!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/15208.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!