Java并发 — 线程池相关问题

时间:2021-7-4 作者:qvyue

线程池是什么,有什么好处?简述线程池中线程复用原理?

线程池概述:

  • 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱之前通过 Thread 创建线程时一个线程必须对应一个任务的限制。

  • 线程池采用生产者-消费者设计模式。线程池的使用方是生产者(任务),线程池本身是消费者,阻塞队列来存储要处理的任务。

  • 线程池主要特点:线程复用;控制最大并发数;管理线程。
    使用线程池的好处:

  • 降低资源消耗:线程复用提高利用率,避免频繁创建和销毁线程(费时且消耗内存等系统资源)

  • 提高响应速度:当任务提交时,可以不用等待创建线程,能立即执行

  • 提高线程的可管理性:线程是稀缺资源,使用线程池可以对线程进行统一分配、调优和监控,更利于线程管理

线程池线程复用原理:在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行

  • 核心原理:线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

创建线程池的方法有哪些?有哪些问题?自定义线程池注意点?

先回忆一下线程创建的方法:

  • 继承Thread类:Thread是类,有单继承的局限性。
  • 实现Runnable接口:任务和线程分开,不能返回执行结果。
  • 实现Callable接口:利用FutureTask执行任务,能通过futureTask.get()取到执行结果。

但是我们工作中一般不这样来创建线程。原因:虽然在 Java 语言中创建线程看上去就像创建一个对象一样简单,只需要 new Thread() 就可以了,但实际上创建线程远不是创建一个对象那么简单。创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。线程池线程复用刚好可以解决这一个问题。

ps:阿里开发手册:【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

从jdk1.5版本开始,在java.uitl.concurrent包下面定义定义了一些与并发相关的类,其中线程池最核心的一个类是ThreadPoolExecutor。

通过 Executors 的静态工厂方法创建线程池的四种方法:

  • newSingleThreadExecutor:单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
  • newFixedThreadExecutor(n):固定数量的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
  • newCacheThreadExecutor:可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
  • newScheduleThreadExecutor:大小无限制的线程池,支持定时和周期性的执行线程

阿里开发手册:【强制】线程池不允许使用Executors去创建,而是通过ThreadPooLExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则(避免上述四种方法的默认实现),规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE可能会堆积大量的请求,导致OOM(out of memory)。
  • CachedThreadPoolScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE可能会创建大量的线程,导致OOM

最终都有可能导致OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列,并设置最大线程数。

如何合理自定义线程池ThreadPoolExecutor:

上述四种线程池的最终方式也是调用的ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                          long keepAliveTime, TimeUnit unit,
                          BlockingQueue workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
  • Runtime.getRuntime().availableProcessors() 查看核心数

  • corePoolSize :1或者0

  • maximumPoolSize,分情况考虑,要看业务是CPU密集型还是IO密集型

  • CPU密集型:该任务需要大量的运算,而没有阻塞,CPU一直全速运行。需要尽可能少的线程,设置:cpu核数+1个线程

  • IO密集型:该任务需要大量的IO,即大量的阻塞,使用多线程可以加速程序运行,如数据库读写。大部分线程都阻塞,故需要多配制线程数。参考设置:CPU核数/(1-阻塞系数阻塞系数在0.8~0.9之间)

创建线程池有哪些参数(构造函数中参数)?

  • corePoolSize :核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,是一种常驻线程
  • maxinumPoolSize :最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
  • keepAliveTime 、 unit :超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间
  • workQueue :任务队列(阻塞队列),用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  • ThreadFactory :线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
  • Handler 任务拒绝策略,有两种情况:

    • 第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。
    • 另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝。

线程池处理任务的流程?

当提交一个新的任务到线程池时,线程池的处理流程如下:

  • 检查核心线程数:如果线程池中正在执行任务的线程数是否达到corePoolSize,如果没有,即使有空闲线程,也会创建一个新的线程执行任务;
  • 检查任务队列:如果核心线程池已满,工作队列已满,将新提交的任务加入工作队列;
  • 检查最大线程数:如果工作队列已满,线程池正在执行的线程数小于最大线程数,就创建一个线程来执行新的任务。否则,执行任务拒绝策略;
Java并发 --- 线程池相关问题

ps:线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。

向线程池提交任务的execute()和submit()方法的区别?

  • 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

  • 返回值(核心):execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执行成功与否;

    submit() ⽅法⽤于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future.get()方法来获取返回值。get()会阻塞当前线程(即调用future.get()的线程)直到任务执行完成。而get(long timeout,TimeUnit unit)方法会阻塞一点时间后返回,此时任务可能还没有执行完。

  • 异常处理:submit()方便Exception处理

线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

阻塞队列作用

  • 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

  • 保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

  • 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源

添加队列而不是先创建最大线程原因:

  • 在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

  • 举例:就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

线程池有哪些拒绝策略?

  • AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

  • DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

  • DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

  • CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

如何关闭线程池?

可以调用 shutdown 或 shutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。

  • shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。

  • shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断没有正在执行任务的线程。

通常调用 shutdown 来关闭线程池,如果任务不一定要执行完可调用 shutdownNow。

参考鸣谢:
https://blog.csdn.net/wolf909867753/article/details/77500625
https://blog.csdn.net/Kurry4ever_/article/details/109294661

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。