为什么要引入线程池?
在未引入线程池之前,我们完成一个任务,需要经过自己创建线程,执行任务,销毁线程这三个步骤。
这存在什么问题呢?
1)浪费资源:线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些系统资源。频繁地创建和销毁线程会浪费大量的系统资源。
2)花费时间变长:线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。
3)线程的不合理分布:在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。
4)无法功能拓展
因此,引入线程池来协调这些线程、拓展功能。
线程池有以下几个作用:
-
降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
-
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
-
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
-
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
了解了为什么要引入线程池之后,我们先来看看线程池要怎么用,再来分析线程池的底层原理。
线程池怎么用?
我们引入线程池,就是为了让线程池去创建线程,然后执行任务。代替我们原先自己手动创建线程。
public class ExecutorCase {
public static void main(String[] args) {
// 创建线程池
Executor executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
// 将任务放进线程池里,在使用者眼里,相当于任务开始执行了。至于线程池是如何用自己创建的线程执行任务的,就是底层的东西了
executor.execute(new Task());
}
}
}
static class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
由上例可以看到,线程池是通过Executors类调用其静态方法创建的。使用Executors类创建出线程池我们称之为内置线程池。
Executors工具类可以创建多种类型的线程池:
-
Executors.newFixedThreadPool:创建固定线程数量的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
-
Executors.newSingleThreadExecutor:创建只有一个线程的线程池。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
-
Executors.newCachedThreadPool:创建可根据实际情况调整线程数量的线程池。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
-
Executors.newScheduledThreadPool:创建给定的延迟后运行任务或者定期执行任务的线程池。
仔细观察上面几种在Executors类中已经写好的线程池,可以发现它们方法中基于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();
// 其他代码····
}
下面我们来分析ThreadPoolExecutor构造器里的参数:
-
corePoolSize:表示常驻核心线程数。如果等于0 ,则任务执行完之后没有任何请求进入时销毁线程池的线程,如果大于0 ,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
-
maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。
-
keepAliveTime:示线程池中的线程空闲时间。当空闲时间达到 keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止。也就是说核心线程外的线程才会被销毁。
-
TimeUnit:表示时间单位。 keepAliveTime 的时间单位通常是TimeUnit.SECONDS。
-
workQueue:表示缓存队列。当请求的线程数大于 maximumPoolSize 线程进入 BlockingQueue 阻塞队列。阻塞队列有以下类型:
-
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。
-
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene。
-
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene。
-
4、priorityBlockingQuene:具有优先级的无界阻塞队列。
-
-
threadFactory:表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个 factory 增加组名前缀来实现的。
-
handler:表示执行拒绝策略的对象。当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
-
1、AbortPolicy:直接抛出异常,默认策略。
-
2、CallerRunsPolicy:直接在调用execute方法的线程中运行,调用任务的 run() 方法。
-
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务。
-
4、DiscardPolicy:直接丢弃任务。
-
使用ThreadPoolExecutor构造函数创建线程
相比于前面介绍的使用Executors工具类创建线程池,看了这些内置线程池的源码,发现都是通过ThreadPoolExecutor构造函数创建的。于是我们可以自己直接使用ThreadPoolExecutor构造函数创建线程。传入ThreadPoolExecutor构造函数的参数由我们自己定义。
public class UserThreadPool {
public static void main(String[] args) {
// 缓存队列设置固定长度为2,为了快速触发rejectHandler
BlockingQueue queue = new LinkedBlockingQueue(2);
// 假设外部任务线程的来源由机房1和机房2的混合调用
UserThreadFactory f1 = new UserThreadFactory("第1机房");
UserThreadFactory f2 = = new UserThreadFactory("第2机房");
UserRejectHandler handler = new UserRejectHandler();
// 核心线程为1,最大线程为2,为了保证触发rejectHandler
ThreadPoolExecutor threadPoolFirst = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f1, handler);
// 利用第二个线程工厂实例创建第二个线程池
ThreadPoolExecutor threadPoolSecond = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f2, handler);
// 创建400个任务线程
Runnable task = new Task();
for (int i = 0; i < 200; i++) {
threadPoolFirst.execute(task);
threadPoolSecond.execute(task);
}
}
}
// 任务执行体
class Task implements Runnable {
private final AtomicLong count = new AtomicLong(0 L);
@Override
public void run() {
System.out.println("running " + count.getAndIncrement());
}
}
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定义线程组名称,在使用jstack来排查线程问题时,非常有帮助
UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName());
return thread;
}
}
从execute源码开始分析
从任务的角度分析
任务提交
Executor.execute()
任务执行完没有返回值。从方法中参数类型可以看出使用这种方式提交的任务必须实现Runnable接口。
ExecutorService.submit()
任务执行完有返回值。
任务调度
进入execute方法,分析任务是怎么被线程池处理的。
检查现在线程池的运行状态(是否为RUNNING)、运行线程数(workerCount )、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
-
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
-
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
-
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
-
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
-
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
由以上过程可以分析出,任务被提交给线程池后,有三种命运:一种情况是任务直接由新创建的线程执行;一种情况是任务被放到阻塞队列里等待空闲线程来申请并执行;一种情况是任务直接被拒绝了。
任务缓冲
线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
任务申请
getTask()方法
整个getTask操作在自旋下完成: 1、workQueue.take:如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行; 2、workQueue.poll:如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;
所以,线程池中实现的线程可以一直执行由用户提交的任务。
任务拒绝
在execute方法中,如果判断出要拒绝这个任务,那么就会执行reject方法。
拒绝策略的那些类实现这个接口,然后重写rejectedExecution方法。这样我们的handler类型就是选择的拒绝策略某个类的类型,调的rejectedExecution方法自然就是重写的方法。
从线程的角度分析
在源码里,Worker类代表线程。
线程增加
addWorker(Runnable firstTask, boolean core);
-
firstTask:指定新增的线程执行的第一个任务,可以为空。
-
core:
true
:表示在新增线程时会判断当前活动线程数是否<corePoolSize,false
:表示新增线程前需要判断当前活动线程数是否<maximumPoolSize。
addWorker执行流程:
1、判断线程池状态,如果线程池状态还未停止,那么执行(2)
2、判断此时线程池里的线程数,如果小于core,则跳出循环,开始创建新的线程。具体实现如下:
3、创建一个Woker对象,表示一个新线程。用ReentrantLock加锁,使得这个线程安全添加进HashSet里。
Woker类:
-
继承了AQS类。
-
实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;
-
当前提交的任务firstTask作为参数传入Worker的构造方法;
-
有thread属性。当我们new一个worker的时候,相当于底层帮我们用线程工厂创建了一个线程。
线程执行任务
addWorker方法里调用 t.start(); 底层执行的就是Worker里的run方法。run方法里再调用runWorker方法。
1、获取第一个任务firstTask,执行该任务。执行任务前会加锁,执行完后释放锁。
2、该线程将第一个执行完后,就循环去阻塞队列里获取任务执行,即getTask方法。如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源。
线程回收
Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
参考
《码出高效:Java开发手册》
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)