带你整理面试过程中关于多线程中的线程池的相关知识点
Posted 南淮北安
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于多线程中的线程池的相关知识点相关的知识,希望对你有一定的参考价值。
文章目录
一、线程池的工作原理
线程是非常宝贵的计算资源,在每次需要时创建并在运行结束后销毁是非常浪费资源的。
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源。
Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行
二、线程复用
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。
前面说过,在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。
可以将在循环方法中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。这样就简单实现了一个线程池,达到了线程复用的效果。
三、线程池的核心组件和核心类
详细内容学习可参考:一篇文章带你深入了解多线程中核心线程池的内部实现
Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor
、Callable、Future、FutureTask这几个核心类,具体的继承关系如图3-2所示。
其中,ThreadPoolExecutor是构建线程的核心方法,该方法的定义如下:
ThreadPoolExecutor构造函数的具体参数:
corePoolSize
:指定了线程池中的线程数量。maximumPoolSize
:指定了线程池中的最大线程数量。keepAliveTime
:当线程池线程数量超过corePoolSize
时,多余的空闲线程的存活时间,即超过corePoolSize
的空闲线程,在多长时间内会被销毁。unit
:keepAliveTime的单位。workQueue
:任务队列,被提交但尚未被执行的任务。threadFactory
:线程工厂,用于创建线程,一般用默认的即可。handler
:拒绝策略。当任务太多来不及处理时,如何拒绝任务。
实际生产时推荐ThreadPoolExecutor
自己手动创建线程池,这样写的方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
线程池里的线程数需要从业务考虑,是CPU密集还是IO密集,假设运行应用的机器CPU核心数是N,CPU密集可以先给到N+1,IO密集可以给到2N
线程的数量不是越大越好,具体需要通过压测进行确定,多线程的目的是为了充分利用CPU的资源,如果线程数量过多,会带来系统的过多开销
四、Java线程池的工作流程
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()
添加一个任务时,线程池会按照以下流程执行任务。
- 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
- 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
- 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
- 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
- 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
- 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
具体的流程如图3-3所示。
五、线程池的拒绝策略
若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务
JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。
(1)AbortPolicy
AbortPolicy 直接抛出异常,阻止线程正常运行,具体的JDK源码如下:
(2)CallerRunsPolicy
CallerRunsPolicy策略:这个策略不会把线程丢弃,而是交给调用线程去执行
(3)DiscardOldestPolicy
DiscardOldestPolicy的拒绝策略为:移除线程队列中最早的一个线程任务,并尝试提交当前任务。具体的JDK实现源码如下:
(4)DiscardPolicy
DiscardPolicy的拒绝策略为:丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。
(5)自定义拒绝策略
以上 4 种拒绝策略均实现了RejectedExecutionHandler
接口,若无法满足实际需要,则用户可以自己扩展RejectedExecutionHandler
接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略。
RejectedExecutionHandler`的定义如下:
其中r为请求执行的任务,executor为当前的线程池。
import java.util.concurrent.*;
public class RejectThreadPoolDemo
public static class MyTask implements Runnable
@Override
public void run()
System.out.println(System.currentTimeMillis() + "thread id:" + Thread.currentThread().getId());
try
Thread.sleep(100);
catch (InterruptedException e)
e.printStackTrace();
public static void main(String[] args) throws InterruptedException
MyTask task = new MyTask();
//自定义线程池:5个常驻线程,最大线程数量也是5个
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
//10个容量的等待队列
new LinkedBlockingDeque<Runnable>(10),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler()
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
System.out.println(r.toString() + " is discard");
);
for (int i = 0; i < Integer.MAX_VALUE; i++)
es.submit(task);
Thread.sleep(10);
由于在这个案例中,MyTask执行需要花费100毫秒,因此,必然会导致大量的任务被直接丢弃。执行上述代码,输出如下:
可以看到,在执行几个任务后,拒绝策略就开始生效了。在实际应用中,我们可以将更详细的信息记录到日志中,来分析系统的负载和任务丢失的情况
六、5中常用的线程池
Java定义了Executor接口并在该接口中定义了execute()
用于执行一个线程任务,然后通过ExecutorService实现Executor接口并执行具体的线程操作。
以上成员均在java.util.concurrent
包中,是JDK并发包的核心类。其中,ThreadPoolExecutor
表示一个线程池。Executors
类则扮演着线程池工厂的角色,通过Executors
可以取得一个拥有特定功能的线程池。
从UML图中亦可知,ThreadPoolExecutor
类实现了Executor
接口,因此通过这个接口,任何Runnable
的对象都可以被ThreadPoolExecutor
线程池调度。
ExecutorService接口有多个实现类可用于创建不同的线程池,如表3-2所示是5种常用的线程池。
1. newCachedThreadPool
newCachedThreadPool用于创建一个缓存线程池。之所以叫缓存线程池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则重新创建一个新的线程并将其添加到线程池中。对于执行时间很短的任务而言,newCachedThreadPool线程池能很大程度地重用线程进而提高系统的性能。
在线程池的keepAliveTime时间超过默认的60秒后,该线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool将不会占用系统的线程资源。
在创建线程时需要执行申请CPU和内存、记录线程状态、控制阻塞等多项工作,复杂且耗时。因此,在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool能够很好地复用运行中的线程(任务已经完成但未关闭的线程)资源来提高系统的运行效率。具体的创建方式如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
2. newFixedThreadPool
newFixedThreadPool用于创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。在newFixedThreadPool线程池中,若处于活动状态的线程数量大于等于核心线程池的数量,则新提交的任务将在阻塞队列中排队,直到有可用的线程资源,具体的创建方式如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
3. newScheduledThreadPool
newScheduledThreadPool创建了一个可定时调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个线程任务:
4. newSingleThreadExecutor
newSingleThreadExecutor线程池会保证该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
在该线程停止或发生异常时,newSingleThreadExecutor线程池会启动一个新的线程来代替该线程继续执行任务:
5. newWorkStealingPool
newWorkStealingPool创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。
这里所说的有足够的线程指JDK根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。
ExecutorService newWorkStealingPool = Executors.newWorkStealingPool();
六、面试题
- 线程池的优点?
- 设计一个线程池的思路?
(1)准备一个任务容器;
(2)一次性启动10个消费者线程;
(3)刚开始任务容器是空的,所以线程都wait在上面;
(4)直到一个外部线程往这个任务容器扔了一个“任务”,就会有一个消费者线程被唤醒notify;
(5)这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来
(6)如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
- 为什么要用数据库连接池,几个参数介绍(阿里)?
以上是关于带你整理面试过程中关于多线程中的线程池的相关知识点的主要内容,如果未能解决你的问题,请参考以下文章
带你整理面试过程中关于 Java 中的集合 List,Queue,Set的相关知识点
带你整理面试过程中关于JVM 的运行机制多线程和 JVM 的内存区域的相关知识点
带你整理面试过程中关于 JVM 中分代收集算法分区收集算法和垃圾收集器的相关知识
带你整理面试中关于HashCodeHashMapConcurrnetHashMapHashTableTreeMapLinkedHashMap的相关知识点