搞懂线程池
一、为什么要使用线程池
1.1 活跃线程过多会导致OOM
创建完线程后,线程是需要内存去放的,一个线程对应一个Thread对象。我们知道,对象是会占用JVM中的堆内存的空间。所创建的线程越多,线程的上下文切换不仅影响性能,它占用的内存也就越多,可能导致OOM,代码如下:
public class ThreadPoolDemo {
public static void show(){
for(int i=0;;i++){//死循环,一直创建线程并启动。
new Thread(()->{
try {
Thread.sleep(TimeUnit.SECONDS.toSeconds(Integer.MAX_VALUE));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("************* "+i);
}
}
}
执行结果:
1.2 线程不能复用
普通的创建线程的方法执行任务不能连续执行,在完成任务之后,线程会被销毁。需要完成别的任务,又需要创建新的线程。而线程创建与线程销毁是耗费资源耗费时间的操作,线程不能复用是对系统资源的浪费。传统的创建线程的方法,完成不同的任务,如下所示:
public class ThreadPoolDemo {
public static void show(){
//创建任务 需要完成任务的顺序 task1->task2->task3
Task task1 = new Task();
Task task2 = new Task();
Task task3 = new Task();
//必须创建3个线程,才能完成任务要求
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
Thread thread3 = new Thread(task3);
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
public class Task implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
1.3 线程池的引入
线程池是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。总的来说,使用线程池有以下三个优点:
- 降低系统资源的消耗。线程池起到线程复用的作用,不用创建过多的线程,也就不会消耗太多系统资源。
- 提高响应速度。线程池中的线程,其中的核心线程,一直都存在,不用创建也不会销毁。当并发任务到来时,提高了响应速度。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、如何使用线程池
2.1 线程池的相关类与接口
Java线程池核心的实现类是ThreadPoolExecutor,我们想要创建自己的线程池时,可以使用这个类。ThreadPoolExecutor实现的顶层接口是Executor。顶层接口Executor提供了一种思想:任务提交和任务执行进行解耦。我们无需关心如何创建线程如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行。ExecutorService接口增加提供了监控线程池的方法和扩充任务执行能力。它们的UML图如下所示:
Executors是快速得到线程池的工具类,创建线程池的工厂类。能够创建各种功能的线程池,比如newFixedThreadPool(int nThreads),是创建一个固定大小、任务队列无界的线程池。newSingleThreadExecutor(),创建的是只有一个线程来执行无界任务的单一线程池。虽然Executors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池。【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。
2.2 ThreadPoolExecutor创建线程池并使用
首先创建线程池对象ThreadPoolExecutor,然后向线程池提交任务调用execute方法,最后关闭线程池。线程池的使用代码如下所示:
public class ThreadPoolDemo {
public static void show(){
//1. 创建线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 5, 60L,
TimeUnit.SECONDS, new ArrayBlockingQueue(5));
//2.提交线程池任务
poolExecutor.execute(
() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("thread Name:%s",Thread.currentThread().getName()));
});
//3.关闭线程池,防止内存泄漏
poolExecutor.shutdown();//设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
//poolExecutor.shutdownNow();//设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
}
}
执行结果:
三、线程池的七大参数
3.1 线程池七大参数
我们在创建ThreadPoolExecutor对象时,发现有四个构造器方法,其中的参数是线程池的七大参数,有5个参数在创建对象时必须给出。它们分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。它们的详细介绍,如下表所示:
参数 | 描述 |
---|---|
corePoolSize | 核心线程数。即使这些线程处于空闲状态,他们也不会被销毁。默认情况下,核心线程会一直存活。 |
maximumPoolSize | 线程池所能容纳的最大线程数。当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到任务队列中。当活跃线程数达到该数值后,后续的新任务将会阻塞。 |
keepAliveTime | 线程闲置超时时长。一个线程如果处于空闲状态,如果超过该时长,非核心线程就会被回收。 |
unit | 指定 keepAliveTime 参数的时间单位。 |
workQueue | 任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。 |
threadFactory(非必需) | 线程工厂。用于指定为线程池创建新线程的方式。 |
handler(非必需) | 拒绝策略。当达到最大线程数时需要执行的饱和策略。 |
3.2 线程池拒绝策略
当任务队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的。JDK提供了四种拒绝策略:
- DiscardPolicy(直接丢弃)。在该策略下,线程池直接丢弃任务,什么都不做。
- AbortPolicy(丢弃抛异常)。这是默认策略,该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
- DiscardOldestPolicy(丢弃任务队列中最早任务,换新任务)。该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
- CallerRunsPolicy(调用线程直接运行)。在该策略下,对新任务优先执行,直接调用其run()方法。
四、线程池的原理
线程池的工作原理简单点来概括就是:工作任务可以一直放,直到线程池满了线程池才会拒绝。除了核心线程,其它线程空闲的情况下会被合理回收。正常情况下,线程池中线程的数量会处在corePoolSize与maximumPoolSize的闭区间。下图是线程池工作原理的示意图:
详细的工作流程可以用下面的流程图表示:
五、线程池的分类与选择
上述的ThreadPoolExecutor创建线程池的过程需要我们给定较多的参数,比较复杂。线程池工具类Executors为我们封装了4种常见的功能性线程池:
- 定长线程池(FixedThreadPool):只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。适用于控制线程最大并发数。
- 定时线程池(ScheduledThreadPool ):核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。适用于执行定时或周期性的任务。
- 可缓存线程池(CachedThreadPool):无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。适用于执行大量、耗时少的任务。
- 单线程化线程池(SingleThreadExecutor):只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。不适合并发以及可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
总结:虽然Executors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池,而是通过ThreadPoolExecutor的方式。
弊端:
- FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
xecutors使用起来很方便,不过在阿里编程规范里是强调了慎用Executors创建线程池,而是通过ThreadPoolExecutor的方式。