工作原理:
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好的利用CPU资源。Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行后,线程池调度器会发现可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
线程复用:
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。Java中的线程Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread 类 , 在start方法中不断循环调用传递进的Runnable对象,程序就会不断执行run方法中的代码。可以在循环方法中不断获取放在Queue中的Runnable对象,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。这样就简单实现了一个线程池,达到了线程复用的效果。
核心组件和核心类:
Java线程池主要由以下4个核心组件组成:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中执行具体任务的线程
- 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度
- 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除
Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor 、 Executors 、 ExecutorService 、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类,具体的继承关系下图所示:
其中,ThreadPoolExecutor是构建线程的核心方法,该方法的
定义如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// ......
// 省略了赋值操作
// ......
}
ThreadPoolExecutor构造函数的具体参数如下表所示:
序号 | 参数 | 说明 |
---|---|---|
1 | corePoolSize | 线程池中核心线程的数量 |
2 | maximumPoolSize | 线程池中最大线程的数量 |
3 | keepAliveTime | 当前线程数量超过corePoolSize 时,空闲线程的存活时间 |
4 | unit | keepAliveTime 的时间单位 |
5 | workQueue | 任务队列,被提交但尚未被执行的任务存放的地方 |
6 | threadFactory | 线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂 |
7 | handler | 由于任务过多或其他原因导致线程池无法处理时的任务拒绝策略 |
工作流程:
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务:
- 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务
- 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中
- 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务
- 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线 程池将拒绝执行该线程任务并抛出RejectExecutionException异常
- 在线程任务执行完毕后,该任务将被从任务队列中移除,线程池将从队列中取下一个线程任务继续执行
- 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小
具体的流程如下图所示:
拒绝策略:
若线程池中的线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这 4种,这些拒绝策略在ThreadPoolExecutor中作为内部类提供,默认的拒绝策略是AbortPolicy。这些拒绝策略不能满足应用的需求时,可以自定义拒绝策略。
- AbortPolicy
AbortPolicy直接抛出异常,阻止线程正常运行,具体的JDK源码如下:
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- CallerRunsPolicy
直接在方法的调用线程中执行,除非线程池已关闭,具体的JDK实现源码如下:
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- DiscardPolicy
丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。具体的JDK实现源码如下:
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() {}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy
移除线程队列中最早(老)的一个线程任务,并尝试提交当前任务。具体的JDK实现源码如下:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();// 最早(老)的任务出队列
e.execute(r);
}
}
}
- 自定义拒绝策略(后文手写线程池时再实现)
以上4种拒绝策略均实现了RejectedExecutionHandler接口,若无法满足实际需要,则用户可以自己扩展RejectedExecutionHandler接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略。
5种常用的线程池:
1、newCachedThreadPool(可缓存的线程池)
创建方式:
ExecutorService threadPool = Executors.newCachedThreadPool();
具体源码:
看源码可知,newCachedThreadPool的核心线程数量为0,也就是说创建的都是非核心线程,并且在空闲时间超过60秒后会自动销毁,因此在没有线程任务运行时,newCachedThreadPool将不会占用系统的线程资源。
使用场景:
任务数量密集而每个任务的执行时间短的情况,newCachedThreadPool能够很好地复用运行中的线程(任务已经完成但未关闭的线程)资源来提高系统的运行效率。
2、newFixedThreadPool(固定大小的线程池)
创建方式:
ExecutorService threadPool = Executors.newFixedThreadPool(2);
具体源码:
可以看出,newFixedThreadPool的核心线程数量与最大线程数量大小是一样的,说明创建的都是核心线程,没有非核心线程。
使用场景:
适用于任务数量已知,相对耗时的任务。
3、newSingleThreadExecutor(单个线程的线程池)
创建方式:
ExecutorService threadPool = Executors.newSingleThreadExecutor();
具体源码:
可以看出,newSingleThreadExecutor的核心线程数量和最大线程数量都为1,该线程池会保证永远有且只有一个可用的线 程,在该线程停止或发生异常时,newSingleThreadExecutor线程池会启动一个新的线程来代替该线程继续执行任务。
使用场景:
适用于希望任务按顺序执行的场景。
4、newScheduledThreadPool(可做任务调度的线程池)
创建方式:
具体源码:
newScheduledThreadPool创建了一个可定时调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个线程任务。
使用场景:
适用于需要定时调度定期执行任务的场景。
5、newWorkStealingPool(工作窃取线程池,JDK1.8新增)
创建方式:
public class Main {
static class Task implements Runnable{
private int time;
public Task(int time){this.time = time;}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + time);
}
}
public static void main(String[] args) throws IOException {
// CPU 核数
int num = Runtime.getRuntime().availableProcessors();
System.out.println(num);
// workStealingPool会自动启动cpu核数个线程去执行任务
ExecutorService service = Executors.newWorkStealingPool();
service.execute(new Task(1000));
// 有一个任务会进行等待,当第一个执行完毕后,会再次偷取任务执行
for (int i = 0; i < num; i++) {
service.execute(new Task(2000));
}
// 因为work stealing 是deamon线程,即后台线程,精灵线程,守护线程
// 所以当main方法结束时, 此方法虽然还在后台运行,但是无输出
// 可以通过对主线程阻塞解决
System.in.read();
}
}
具体源码:
可以看出,WorkStealingPool背后是使用ForkJoinPool实现的,构造函数的第一个参数就是系统CPU核数,这样很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。在内部通过使用多个队列来减少各个线程调度产生的竞争。当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。
好了,5种线程池就介绍完了,下面随我一步步实现一个自定义的线程池:
1、定义一个阻塞队列用于存放和获取任务
class BlockingQueue<T> {
//任务队列
private Deque<T> queue = new ArrayDeque<>();
//锁
private ReentrantLock lock = new ReentrantLock();
//生产者条件变量
private Condition fullWaitSet = lock.newCondition();
//消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
//容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
//阻塞获取任务
public T take(){
lock.lock();
try{
while (queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
//阻塞添加任务
public void put(T task){
lock.lock();
try{
while (queue.size() == capcity){
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(task);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//判断队列是否满
if (queue.size() == capcity) {
rejectPolicy.reject(this,task);
}else { //有空闲
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
//获取大小
public int size(){
lock.lock();
try{
return queue.size();
}finally {
lock.unlock();
}
}
}
由于篇幅原因,该阻塞队列仅提供了阻塞的添加获取任务的基本方法,后续可完善。
2、定义工作线程
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task){
this.task = task;
}
@Override
public void run(){
//当task不为空,执行任务
//当task执行完毕,再接着从任务队列中获取任务并执行
while (task != null || (task = taskQueue.take()) != null){
try{
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
}
}
该类是线程池类的内部类,这里先定义好。
3、定义拒绝策略接口
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue,T task);
}
4、定义线程池类
class ThreadPool{
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
// 拒绝策略
private RejectPolicy<Runnable> rejectPolicy;
//执行任务
public void execute(Runnable task){
//当线程数没有超过 coreSize时,新建worker 对象执行
//如果任务数超过 coreSize时,加入任务队列暂存
synchronized(workers){
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
workers.add(worker);
worker.start();
}else {
taskQueue.tryPut(rejectPolicy,task);
}
}
}
public ThreadPool(int coreSize, int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
// 前面已介绍,这里省略了相关属性和方法
}
}
5、测试类
public class TestThreadPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(2, 10
,(queue,task) -> {
//死等
//queue.put(task);
//让调用者抛出异常
//throw new RuntimeException("任务执行失败 "+task);
//让调用者自己执行任务
task.run();
});
for (int i = 0;i<5;i++){
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
好了,自定义的线程池实现完毕,虽然写的比较简陋,但线程池的大致运行流程如此。
至此,Java中的线程池详解完毕,最后介绍一下常用的7种阻塞队列并附上一张类图
名称 | 说明 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列 |
SynchronousQueue | 一个不存储元素的阻塞队列 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列 |