网易一面:如何设计线程池?请手写一个简单线程池?
说在前面
在40岁老架构师 尼恩的读者社区(50+)中,最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易的面试资格,遇到了几个很重要的面试题:
如何设计线程池?
与之类似的、其他小伙伴遇到过的问题还有:
请手写一个简单线程池?
这个问题,很多小伙伴在社群里边反馈,都遇到过
线程池的知识,既是面试的核心知识,又是开发的核心知识。
所以,这里尼恩给大家做一下系统化、体系化的线程池梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V62版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从公众号 【技术自由圈】获取。
为什么要使用线程池?
多线程编程是在开发过程中非常基础且非常重要的一个环节,基本上任何一家软件公司或者项目中都会使用多线程。主要有三个原因:
- 降低资源的消耗。降低线程创建和销毁的资源消耗。
- 提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间
- 提高线程的可管理性
总之:线程池是一种常用的并发编程工具,它可以帮助我们更好地管理和复用线程资源,提高程序的性能和稳定性。
线程池也是 3高架构的基础技术。
JAVA中的线程池组件
在Java中,我们可以使用java.util.concurrent包中提供的ThreadPoolExecutor类来创建和使用线程池。
ThreadPoolExecutor 是非常高频,非常常用的组件。
对于 ThreadPoolExecutor 的底层原理和源码,大家要做到非常深入的掌握。大家一定要深入看ThreadPoolExecutor线程池源码,了解其执行过程。
另外,看懂线程池执行流程和源码设计有助于提升我们多线程编程技术和解决工作中遇到的问题。
手写线程池的重要性
很多小伙伴给尼恩反馈, 说线程池的源码太难,看不懂。
怎么办呢?
大家可以先易后难。
可以手撸一个简单版的线程池加强一下对执行流程的理解。然后再深入源码去历险记。
或者说,如果我们想要更好地理解线程池的工作原理,那么自己动手实现一个简单的线程池是一个很好的选择。
接下来,我将带领大家一步一步地实现一个简单的线程池。
我们将从最基本的功能开始,逐步添加更多的功能和优化,最终实现一个完整的线程池。
线程池的实现原理
线程池是一个典型的生产者-消费者模型。下图所示为线程池的实现原理:
- 调用方不断向线程池中提交任务; (生产者)
- 线程池中有一组线程,不断地从队列中取任务,(消费者)
- 线程池管理一个任务队列,对 异步任务进行缓冲 (缓冲区)
要实现一个线程池,有几个问题需要考虑:
- 队列设置多长?
如果是无界的,调用方不断往队列中方任务,可能导致内存耗尽。如果是有界的,当队列满了之后,调用方如何处理? - 线程池中的线程个数是固定的,还是动态变化的?
- 每次提交新任务,是放入队列?还是开新线程
- 当没有任务的时候,线程是睡眠一小段时间?还是进入阻塞?如果进入阻塞,如何唤醒?
针对问题4,有3种做法:
- 不使用阻塞队列,只使用一般的线程安全的队列,也无阻塞/唤醒机制。
当队列为空时,线程池中的线程只能睡眠一会儿,然后醒来去看队列中有没有新任务到来,如此不断轮询。 - 不使用阻塞队列,但在队列外部,线程池内部实现了阻塞/唤醒机制
- 使用阻塞队列
很显然,做法3最完善,既避免了线程池内部自己实现阻塞/唤醒机制的麻烦,也避免了做法1的睡眠/轮询带来的资源消耗和延迟。
现在来带大家手写一个简单的线程池,让大家更加理解线程池的工作原理
手写一个简单线程池
第一步:定义线程池接口
首先,我们需要定义一个线程池接口,用来表示线程池应该具备哪些功能。
一个简单的线程池应该至少具备以下几个功能:
- 添加任务并执行
- 关闭线程池
- 强制关闭线程池
因此,我们可以定义一个ThreadPool接口,它包含三个方法:execute、shutdown和shutdownNow。
import java.util.List;// 线程池接口
public interface ThreadPool {// 提交任务到线程池void execute(Runnable task);// 优雅关闭void shutdown();//立即关闭List<Runnable> shutdownNow();
}
其中:
- execute方法用来添加任务并执行;
- shutdown方法用来关闭线程池,它会等待已经提交到线程池中的任务都执行完毕后再关闭;
- shutdownNow方法用来强制关闭线程池,它会立即停止所有正在执行或等待执行的任务,并返回未执行的任务列表。
第二步:实现线程的池化管理
接下来,我们需要实现一个简单的线程池类,它实现了ThreadPool接口,并提供了基本的功能。
为了简化代码,先实现一个v1版本,这是一个基础版本,一个简单的实现示例。
在v1版本中,我们先不考虑拒绝策略、自动调节线程资源等高级功能。
下面是一个简单的实现示例:
首先定义一个工作线程类:
// 定义一个工作线程类
public class WorkerThread extends Thread {// 用于从任务队列中取出并执行任务private BlockingQueue<Runnable> taskQueue;// 构造方法,传入任务队列public WorkerThread(BlockingQueue<Runnable> taskQueue) {this.taskQueue = taskQueue;}// 重写run方法@Overridepublic void run() {// 循环执行,直到线程被中断while (!Thread.currentThread().isInterrupted() && !taskQueue.isEmpty()) {try {// 从任务队列中取出一个任务,如果队列为空,则阻塞等待Runnable task = taskQueue.take();// 执行任务task.run();} catch (Exception e) {e.printStackTrace();// 如果线程被中断,则退出循环break;}}}
}
然后, 基于一个线程池接口,实现一个简单的线程池,
// 简单的线程池实现
public class SimpleThreadPool implements ThreadPool {// 线程池初始化时的线程数量private int initialSize;// 任务队列private BlockingQueue<Runnable> taskQueue;// 用于存放和管理工作线程的集合private List<WorkerThread> threads;// 是否已经被shutdown标志private volatile boolean isShutdown = false;public SimpleThreadPool(int initialSize) {this.initialSize = initialSize;taskQueue = new LinkedBlockingQueue<>();threads = new ArrayList<>(initialSize);// 初始化方法,创建一定数量的工作线程,并启动它们for (int i = 0; i < initialSize; i++) {WorkerThread workerThread = new WorkerThread(taskQueue);workerThread.start();threads.add(workerThread);}}// 实现execute方法,用于将任务加入到任务队列,并通知工作线程来执行@Overridepublic void execute(Runnable task) {if (isShutdown) {throw new IllegalStateException("ThreadPool is shutdown");}taskQueue.offer(task);}// 关闭线程池, 等待所有线程执行完毕@Overridepublic void shutdown() {// 修改状态isShutdown = true;for (WorkerThread thread : threads) {// 中断线程thread.interrupt();}}@Overridepublic List<Runnable> shutdownNow() {// 修改状态isShutdown = true;// 清空队列List<Runnable> remainingTasks = new ArrayList<>();taskQueue.drainTo(remainingTasks);// 中断所有线程for (WorkerThread thread : threads) {thread.interrupt();}// 返回未执行任务集合return remainingTasks;}
}
这个版本的线程池实现了基本的添加任务并执行、关闭线程池和强制关闭线程池等功能。
它在构造方法中接收一个初始化线程池大小参数,用于初始化任务队列和工作线程集合,并创建一定数量的工作线程。
第三步:自定义线程池的基本参数
在上一步中,我们实现了一个简单的线程池,它具备了基本的功能。
但是,它存在一个问题:任务队列没有指定容量大小,是个无界队列,其次只指定了初始的线程池大小,应该要提供根据不同的应用场景来调整线程池的大小参数,以提高性能和资源利用率。
因此线程池实现类需要实现自定义初始大小、最大大小以及核心大小的功能。
- 初始大小是指线程池初始化时创建的工作线程数量
- 最大大小是指线程池能够容纳的最多的工作线程数量
- 核心大小是指线程池在没有任务时保持存活的工作线程数量。
这三个参数需要在基本的线程池实现类中定义为成员变量,并在构造方法中传入并赋值。
同时,还需要在execute方法中根据这三个参数来动态地调整工作线程的数量,例如:
- 当活跃的工作线程数量小于核心大小时,尝试创建并启动一个新的工作线程来执行任务;
- 当活跃的工作线程数量大于等于核心大小时,将任务加入到任务队列,等待空闲的工作线程来执行;
- 当任务队列已满时,尝试创建并启动一个新的工作线程来执行任务,
- 当活跃的工作线程数量达到最大大小时,无法再创建新的工作线程。
我们还需要在构造方法里提供一个参数queueSize,用于限制队列大小。
下面我们就对类进行改造:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;public class SimpleThreadPool implements ThreadPool {// 线程池初始化时的线程数量private int initialSize;// 线程池最大线程数量private int maxSize;// 线程池核心线程数量private int coreSize;// 队列大小private int queueSize;// 任务队列private BlockingQueue<Runnable> taskQueue;// 用于存放和管理工作线程的集合private List<WorkerThread> threads;// 是否已经被shutdown标志private volatile boolean isShutdown = false;public SimpleThreadPool(int initialSize, int maxSize, int coreSiz, int queueSizee) {// 初始化参数this.initialSize = initialSize;this.maxSize = maxSize;this.coreSize = coreSize;taskQueue = new LinkedBlockingQueue<>(queueSize);threads = new ArrayList<>(initialSize);// 初始化方法,创建一定数量的工作线程,并启动它们for (int i = 0; i < initialSize; i++) {WorkerThread workerThread = new WorkerThread();workerThread.start(taskQueue);threads.add(workerThread);}}@Overridepublic void execute(Runnable task) {if (isShutdown) {throw new IllegalStateException("ThreadPool is shutdown");}// 当线程数量小于核心线程数时,创建新的线程if (threads.size() < coreSize) {addWorkerThread(task);} else if (!taskQueue.offer(task)) {// 当队列已满时,且线程数量小于最大线程数量时,创建新的线程if (threads.size() < maxSize) {addWorkerThread(task);} else {throw new IllegalStateException("执行任务失败");}}}// 创建新的线程,并执行任务private void addWorkerThread(Runnable task) {WorkerThread workerThread = new WorkerThread();workerThread.start(taskQueue);threads.add(workerThread);// 任务放入队列taskQueue.offer(task);}省略其它代码}
这一步,我们在 SimpleThreadPool里新增了initialSize,maxSize, coreSize 三个变量,在构造方法里传入对应三个参数,同时在execute方法里,当有任务进入时,先判断当前线程池数量是否满足不同条件,进而执行不同的处理逻辑。
第四步:设计饱和拒绝策略
这个功能是为了处理当任务队列已满且无法再创建新的工作线程时,也是就线程池的工作量饱和时,如何处理被拒绝的任务。
不同的场景可能需要不同的拒绝策略,例如
- 直接抛出异常
- 忽略任务
- 阻塞当前线程
- 等等
为了让用户可以自定义拒绝策略,需要
- 定义一个拒绝策略接口,声明一个方法,用于处理被拒绝的任务。
- 然后需要在基本的线程池实现类中定义一个拒绝策略成员变量,并在构造方法中传入并赋值。
- 最后,在execute方法中,在无法创建新的工作线程时,调用拒绝策略来处理该任务。
下面是一个简单的实现示例,
我们首先定义了一个RejectedExecutionHandler接口,用来表示拒绝策略。用户可以根据需要实现这个接口,并在构造线程池时传入自己的拒绝策略。
public interface RejectedExecutionHandler {// 参数:r 代表被拒绝的任务,executor 代表线程池对象void rejectedExecution(Runnable r, ThreadPool executor);
}
我们再实现一个直接抛出异常的拒绝策略
// 直接抛出异常的拒绝策略
public class AbortPolicy implements RejectedExecutionHandler {public void rejectedExecution(Runnable r, ThreadPool executor) {throw new RuntimeException("The thread pool is full and the task queue is full.");}
}
我们也可以实现一个丢弃策略:
// 忽略任务的拒绝策略
public class DiscardPolicy implements RejectedExecutionHandler {public void rejectedExecution(Runnable r, ThreadPool executor) {// do nothingSystem.out.println("Task rejected: " + r);}
}
接下来,我们再优化SimpleThreadPool类,
public class SimpleThreadPool implements ThreadPool {// 线程池初始化时的线程数量private int initialSize;// 线程池最大线程数量private int maxSize;// 线程池核心线程数量private int coreSize;// 队列大小private int queueSize;// 任务队列private BlockingQueue<Runnable> taskQueue;// 用于存放和管理工作线程的集合private List<WorkerThread> threads;// 是否已经被shutdown标志private volatile boolean isShutdown = false;// 默认的拒绝策略private final static RejectedExecutionHandler DEFAULT_REJECT_HANDLER = new AbortPolicy();// 拒绝策略成员变量private final RejectedExecutionHandler rejectHandler;public SimpleThreadPool(int initialSize, int maxSize, int coreSize, int queueSize) {this(initialSize, maxSize, coreSize, queueSize, DEFAULT_REJECT_HANDLER);}public SimpleThreadPool(int initialSize, int maxSize, int coreSize , int queueSize, RejectedExecutionHandler rejectHandler) {System.out.printf("初始化线程池: initialSize: %d, maxSize: %d, coreSize: %d%n", initialSize, maxSize, coreSize);// 初始化参数this.initialSize = initialSize;this.maxSize = maxSize;this.coreSize = coreSize;taskQueue = new LinkedBlockingQueue<>(queueSize);threads = new ArrayList<>(initialSize);this.rejectHandler = rejectHandler;// 初始化方法,创建一定数量的工作线程,并启动它们for (int i = 0; i < initialSize; i++) {WorkerThread workerThread = new WorkerThread(taskQueue);workerThread.start();threads.add(workerThread);}}@Overridepublic void execute(Runnable task) {System.out.printf("添加任务: %s%n", task.toString());if (isShutdown) {throw new IllegalStateException("ThreadPool is shutdown");}// 当线程数量小于核心线程数时,创建新的线程if (threads.size() < coreSize) {addWorkerThread(task);System.out.printf("创建新的线程: thread count: %d, number of queues: %d%n", threads.size(), taskQueue.size());} else if (!taskQueue.offer(task)) {// 当队列已满时,且线程数量小于最大线程数量时,创建新的线程if (threads.size() < maxSize) {addWorkerThread(task);System.out.printf("创建新的线程: thread count: %d, number of queues: %d%n", threads.size(), taskQueue.size());} else {//使用拒绝策略rejectHandler.rejectedExecution(task, this);}}}// 省略其它代码
}
这个版本的线程池在构造方法中新增了一个handler参数,用来表示拒绝策略。当任务队列已满时,它会调用handler的rejectedExecution方法来处理被拒绝的任务。
第五步:性能优化之:自动调节线程资源
到目前为止,我们已经实现了一个简单但功能完备的线程池。
但是,它还有很多可以优化和扩展的地方。
例如,可以添加自动调节线程资源的功能,为啥需要 自动调节 线程资源呢?
因为线程资源,是非常昂贵的。
自动调节 线程资源功能是为了让线程池可以根据任务的变化,动态地增加或减少工作线程的数量,以提高性能和资源利用率。
为了实现这个功能,需要在基本的线程池实现类中定义一个空闲时长成员变量,并在构造方法中传入并赋值。
空闲时长是指当工作线程没有任务执行时,可以保持存活的时间。
如果超过这个时间还没有新的任务,那么工作线程就会自动退出。
同时,还需要在工作线程类中定义一个空闲开始时间成员变量,并在run方法中更新它。
空闲开始时间是指当工作线程从任务队列中取出一个任务后,上一次取出任务的时间。
如果当前时间减去空闲开始时间大于空闲时长,那么工作线程就会自动退出。
ok,那么我们继续改进线程池,
public class SimpleThreadPool implements ThreadPool {// 省略其它代码// 线程空闲时长private long keepAliveTime;public SimpleThreadPool(int initialSize, int maxSize, int coreSize, int queueSize, long keepAliveTime) {this(initialSize, maxSize, coreSize, queueSize, keepAliveTime, DEFAULT_REJECT_HANDLER);}public SimpleThreadPool(int initialSize, int maxSize, int coreSize, int queueSize, long keepAliveTime, RejectedExecutionHandler rejectHandler) {System.out.printf("初始化线程池: initialSize: %d, maxSize: %d, coreSize: %d%n", initialSize, maxSize, coreSize);// 初始化参数this.initialSize = initialSize;this.maxSize = maxSize;this.coreSize = coreSize;taskQueue = new LinkedBlockingQueue<>(queueSize);threads = new ArrayList<>(initialSize);this.rejectHandler = rejectHandler;this.keepAliveTime = keepAliveTime;// 初始化方法,创建一定数量的工作线程,并启动它们for (int i = 0; i < initialSize; i++) {// 传入相关参数到工作线程WorkerThread workerThread = new WorkerThread(keepAliveTime, taskQueue, threads);workerThread.start();threads.add(workerThread);}}// 省略其它代码
}
然后改造工作线程WorkerThread:
// 定义一个工作线程类
public class WorkerThread extends Thread {private List<WorkerThread> threads;// 空闲时长private long keepAliveTime;// 用于从任务队列中取出并执行任务private BlockingQueue<Runnable> taskQueue;// 构造方法,传入任务队列public WorkerThread(long keepAliveTime, BlockingQueue<Runnable> taskQueue, List<WorkerThread> threads) {this.keepAliveTime = keepAliveTime;this.taskQueue = taskQueue;this.threads = threads;}// 重写run方法@Overridepublic void run() {long lastActiveTime = System.currentTimeMillis();// 循环执行,直到线程被中断Runnable task;while (!Thread.currentThread().isInterrupted() && !taskQueue.isEmpty()) {try {// 从任务队列中取出一个任务,如果队列为空,则阻塞等待task = taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS);if (task != null) {task.run();System.out.printf("WorkerThread %d, current task: %s%n", Thread.currentThread().getId(), task.toString());lastActiveTime = System.currentTimeMillis();} else if (System.currentTimeMillis() - lastActiveTime >= keepAliveTime) {// 从线程池中移除threads.remove(this);System.out.printf("WorkerThread %d exit %n", Thread.currentThread().getId());break;}} catch (Exception e) {// 从线程池中移除threads.remove(this);e.printStackTrace();// 如果线程被中断,则退出循环break;}}}
}
在WorkerThread类run方法里,采用taskQueue.poll
方法指定等待时长,这里是线程退出的关键。
如果超时未获取到任务,则表明当前线程长时间未处理任务,可以正常退出,并从线程池里移除该线程。
手写一个简单线程池的完整代码
下面,我们给出完整的所有代码:
拒绝策略相关类:
// 拒绝策略接口
public interface RejectedExecutionHandler {// 参数:r 代表被拒绝的任务,executor 代表线程池对象void rejectedExecution(Runnable r, ThreadPool executor);
}// 忽略任务的拒绝策略
public class DiscardPolicy implements RejectedExecutionHandler {public void rejectedExecution(Runnable r, ThreadPool executor) {// do nothingRunnableWrapper wrapper = (RunnableWrapper) r;System.out.println("Task rejected: " + wrapper.getTaskId());}
}
为了通过输出日志,清晰的展现线程池中任务的运行流程,新增了RunnableWrapper用于记录taskId,方便日志监控。
public class RunnableWrapper implements Runnable{private final Integer taskId;public RunnableWrapper(Integer taskId) {this.taskId = taskId;}public Integer getTaskId() {return this.taskId;}@Overridepublic void run() {System.out.println("Task " + taskId + " is running.");try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();// ignore}System.out.println("Task " + taskId + " is completed.");}
}
线程池接口
// 线程池接口
public interface ThreadPool {// 提交任务到线程池void execute(Runnable task);// 优雅关闭void shutdown();//立即关闭List<Runnable> shutdownNow();
}
工作线程类:
// 定义一个工作线程类
public class WorkerThread extends Thread {private List<WorkerThread> threads;// 空闲时长private long keepAliveTime;// 用于从任务队列中取出并执行任务private BlockingQueue<Runnable> taskQueue;// 构造方法,传入任务队列public WorkerThread(long keepAliveTime, BlockingQueue<Runnable> taskQueue, List<WorkerThread> threads) {this.keepAliveTime = keepAliveTime;this.taskQueue = taskQueue;this.threads = threads;}// 重写run方法@Overridepublic void run() {long lastActiveTime = System.currentTimeMillis();// 循环执行,直到线程被中断Runnable task;while (!Thread.currentThread().isInterrupted() || !taskQueue.isEmpty()) {try {// 从任务队列中取出一个任务,如果队列为空,则阻塞等待task = taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS);RunnableWrapper wrapper = (RunnableWrapper) task;if (task != null) {System.out.printf("WorkerThread %s, poll current task: %s%n", Thread.currentThread().getName(), wrapper.getTaskId());task.run();lastActiveTime = System.currentTimeMillis();} else if (System.currentTimeMillis() - lastActiveTime >= keepAliveTime) {// 从线程池中移除threads.remove(this);System.out.printf("WorkerThread %s exit %n", Thread.currentThread().getName());break;}} catch (Exception e) {// 从线程池中移除System.out.printf("WorkerThread %s occur exception%n", Thread.currentThread().getName());threads.remove(this);e.printStackTrace();// 如果线程被中断,则退出循环break;}}}
}
简单线程池实现类
public class SimpleThreadPool implements ThreadPool {// 线程池初始化时的线程数量private int initialSize;// 线程池最大线程数量private int maxSize;// 线程池核心线程数量private int coreSize;// 任务队列private BlockingQueue<Runnable> taskQueue;// 用于存放和管理工作线程的集合private List<WorkerThread> threads;// 是否已经被shutdown标志private volatile boolean isShutdown = false;// 默认的拒绝策略private final static RejectedExecutionHandler DEFAULT_REJECT_HANDLER = new AbortPolicy();// 拒绝策略成员变量private final RejectedExecutionHandler rejectHandler;// 线程空闲时长private long keepAliveTime;public SimpleThreadPool(int initialSize, int maxSize, int coreSize, int queueSize, long keepAliveTime) {this(initialSize, maxSize, coreSize, queueSize, keepAliveTime, DEFAULT_REJECT_HANDLER);}public SimpleThreadPool(int initialSize, int maxSize, int coreSize, int queueSize, long keepAliveTime, RejectedExecutionHandler rejectHandler) {System.out.printf("初始化线程池: initialSize: %d, maxSize: %d, coreSize: %d%n", initialSize, maxSize, coreSize);// 初始化参数this.initialSize = initialSize;this.maxSize = maxSize;this.coreSize = coreSize;taskQueue = new LinkedBlockingQueue<>(queueSize);threads = new ArrayList<>(initialSize);this.rejectHandler = rejectHandler;this.keepAliveTime = keepAliveTime;// 初始化方法,创建一定数量的工作线程,并启动它们for (int i = 0; i < initialSize; i++) {WorkerThread workerThread = new WorkerThread(keepAliveTime, taskQueue, threads);workerThread.start();threads.add(workerThread);}}@Overridepublic void execute(Runnable task) {if (isShutdown) {throw new IllegalStateException("ThreadPool is shutdown");}RunnableWrapper wrapper = (RunnableWrapper) task;System.out.printf("put task: %s %n" , wrapper.getTaskId());// 当线程数量小于核心线程数时,创建新的线程if (threads.size() < coreSize) {addWorkerThread(task);System.out.printf("小于核心线程数,创建新的线程: thread count: %d, queue remainingCapacity : %d%n", threads.size(), taskQueue.remainingCapacity());} else if (!taskQueue.offer(task)) {// 当队列已满时,且线程数量小于最大线程数量时,创建新的线程if (threads.size() < maxSize) {addWorkerThread(task);System.out.printf("队列已满, 创建新的线程: thread count: %d, queue remainingCapacity : %d%n", threads.size(), taskQueue.remainingCapacity());} else {rejectHandler.rejectedExecution(task, this);}} else {System.out.printf("任务放入队列: thread count: %d, queue remainingCapacity : %d%n", threads.size(), taskQueue.remainingCapacity());}}// 创建新的线程,并执行任务private void addWorkerThread(Runnable task) {WorkerThread workerThread = new WorkerThread(keepAliveTime, taskQueue, threads);workerThread.start();threads.add(workerThread);// 任务放入队列try {taskQueue.put(task);} catch (InterruptedException e) {throw new RuntimeException(e);}}// 关闭线程池, 等待所有线程执行完毕@Overridepublic void shutdown() {System.out.printf("shutdown thread, count: %d, queue remainingCapacity : %d%n", threads.size(), taskQueue.remainingCapacity());// 修改状态isShutdown = true;for (WorkerThread thread : threads) {// 中断线程thread.interrupt();}}@Overridepublic List<Runnable> shutdownNow() {System.out.printf("shutdown thread now, count: %d, queue remainingCapacity : %d%n", threads.size(), taskQueue.remainingCapacity());// 修改状态isShutdown = true;// 清空队列List<Runnable> remainingTasks = new ArrayList<>();taskQueue.drainTo(remainingTasks);// 中断所有线程for (WorkerThread thread : threads) {thread.interrupt();}// 返回未执行任务集合return remainingTasks;}
}
第六步:验证线程池
接下来,我们编写一个测试用例来验证我们的线程池是否能正常运行。
最后,我们编写了一个测试用例SimpleThreadPoolTest,
// 定义一个测试用例类
public class SimpleThreadPoolTest {public static void main(String[] args) throws InterruptedException {SimpleThreadPool threadPool = new SimpleThreadPool(1, 4, 2, 3, 2000, new DiscardPolicy());for (int i = 0; i < 10; i++) {threadPool.execute(new RunnableWrapper(i));}Thread.sleep(10_000);threadPool.shutdown();}
}
这个测试用例创建了一个拥有1个初始线程、最多4个线程、核心线程数为2、队列长度为3,空闲线程保留时间为2000毫秒的线程池。
它使用了一个简单的拒绝策略,当任务被拒绝时,它会打印一条消息。
然后,测试用例向线程池中提交了10个简单的任务,每个任务都会打印一条消息,然后睡眠100毫秒,再打印一条消息。
最后,测试用例调用了shutdown方法来关闭线程池。
当我们运行这个测试用例时,会看到类似下面的输出:
初始化线程池: initialSize: 1, maxSize: 4, coreSize: 2
put task: 0
小于核心线程数,创建新的线程: thread count: 2, queue remainingCapacity : 2
put task: 1
WorkerThread Thread-1, poll current task: 0
WorkerThread Thread-0, poll current task: 1
Task 1 is running.
任务放入队列: thread count: 2, queue remainingCapacity : 2
put task: 2
Task 0 is running.
任务放入队列: thread count: 2, queue remainingCapacity : 2
put task: 3
任务放入队列: thread count: 2, queue remainingCapacity : 1
put task: 4
任务放入队列: thread count: 2, queue remainingCapacity : 0
put task: 5
WorkerThread Thread-2, poll current task: 2
Task 2 is running.
队列已满, 创建新的线程: thread count: 3, queue remainingCapacity : 0
put task: 6
WorkerThread Thread-3, poll current task: 3
Task 3 is running.
队列已满, 创建新的线程: thread count: 4, queue remainingCapacity : 0
put task: 7
Task rejected: 7
put task: 8
Task rejected: 8
put task: 9
Task rejected: 9
Task 2 is completed.
Task 1 is completed.
WorkerThread Thread-2, poll current task: 4
Task 4 is running.
Task 0 is completed.
Task 3 is completed.
WorkerThread Thread-1, poll current task: 6
WorkerThread Thread-0, poll current task: 5
Task 6 is running.
Task 5 is running.
Task 5 is completed.
Task 6 is completed.
Task 4 is completed.
WorkerThread Thread-3 exit
WorkerThread Thread-1 exit
WorkerThread Thread-0 exit
WorkerThread Thread-2 exit
shutdown thread, count: 0, queue remainingCapacity : 3
从输出中可以看出,线程池中最多有4个线程在同时运行。当有空闲线程时,新提交的任务会被立即执行;否则,新提交的任务会被添加到任务队列中等待执行。
ok,到了这里,大家能够帮助大家更好地理解上文中实现的简单线程池。
接下来,大家可以去进一步技术深入,去研究线程池的源码了。
实操总结
通过实操,我们一步一步地实现了一个简单的线程池。
我们从最基本的功能开始,逐步添加了拒绝策略、自动调节线程资源等高级功能,并对线程池进行了优化。
通过这个过程,我们可以更好地理解线程池的工作原理和实现细节。
当然,这只是一个简单的示例,实际应用中的线程池可能会更加复杂和强大。
上的实操,与Java标准库中提供的线程池相比,仍然存在一些不足之处。
例如:
- 没有提供足够的构造参数和方法,让用户可以更好地控制和监控线程池的行为。
- 没有提供足够多的拒绝策略,让用户可以根据不同的场景选择不同的拒绝策略。
- 没有提供定时任务和周期性任务的执行功能。
- 没有提供足够完善的错误处理机制。
Java标准库中提供的线程池实现(如ThreadPoolExecutor类),则在这些方面都做得更好。
- 它提供了丰富的构造参数和方法,让用户可以更好地控制和监控线程池的行为;
- 它提供了多种拒绝策略,让用户可以根据不同的场景选择不同的拒绝策略;
- 它还提供了ScheduledThreadPoolExecutor类,用来执行定时任务和周期性任务;
- 它还提供了完善的错误处理机制,可以帮助用户更好地处理异常情况。
如果要深入了解Java标准库中提供的线程池实现,可以参考清华大学出版社所出版的尼恩 Java高并发三部曲之二 《Java高并发核心编程 卷2 加强版》。
此书广受好评,写得非常细致,这里不做赘述。
作者介绍:
本文1作: Andy,资深架构师, 《Java 高并发核心编程 加强版》作者之1 。
本文2作: 尼恩,40岁资深老架构师, 《Java 高并发核心编程 加强版 卷1、卷2、卷3》创世作者, 著名博主 。 《K8S学习圣经》《Docker学习圣经》等11个PDF 圣经的作者。
参考文献:
https://www.cnblogs.com/daoqidelv/p/7043696.html
清华大学出版社《Java高并发核心编程 卷2 加强版》
《队列之王 Disruptor 红宝书》
技术自由的实现路径 PDF:
实现你的 架构自由:
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
《响应式圣经:10W字,实现Spring响应式编程自由》
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
《Linux命令大全:2W多字,一次实现Linux自由》
实现你的 网络 自由:
《TCP协议详解 (史上最全)》
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
实现你的 面试题 自由:
4000页《尼恩Java面试宝典 》 40个专题
以上尼恩 架构笔记、面试题 的PDF文件更新,请到《技术自由圈》公号获取↓↓↓
免费获取11个技术圣经 PDF:
相关文章:
网易一面:如何设计线程池?请手写一个简单线程池?
说在前面 在40岁老架构师 尼恩的读者社区(50)中,最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易的面试资格,遇到了几个很重要的面试题: 如何设计线程池? 与之类似的、其他小伙伴遇到过的问题还有: …...
网络安全之密码学
目录 密码学 定义 密码的分类 对称加密 非对称加密 对称算法与非对称算法的优缺点 最佳解决办法 --- 用非对称加密算法加密对称加密算法的密钥 非对称加密如何解决对称加密的困境 密钥传输风险 密码管理难 常见算法 对称算法 非对称算法 完整性与身份认证最佳解决…...
第14章 项目采购管理
文章目录 采购管理包括如下几个过程14.2 编制采购计划 462编制采购计划的输出1)采购管理计划2)采购工作说明书3)采购文件 14.2.3 工作说明书(SOW) 14.3 实施采购 47414.3.2 实施采购的方法和技术 476(1&…...
Vite+Vue下的多页面入口配置
我发现多页面入口配置在网上的资料比较乱,今天正好结合我们的开源API分析工具项目(APIcat)更新情况总结一下。 更新vite.config.js 主要配置的更新是在vite.config.js里面要增加build里的rollupOptions,因为vite底层使用了rollu…...
ChatGPT背后的打工人:你不干,有的是AI干
AI“出圈” 如今,数字技术发展速度惊人,AI提高了社会生产效率,更真切地冲击到原有的生产秩序。 年初AI技术的爆发,让国内看到了进一步降本增效的希望。 国内多家互联网企业相继推出类ChatGPT产品,复旦大学邱锡鹏教授…...
【Access】Access:SQL 语句汇总
目录 一、SQL 的功能 二、考试重点 三、关系的定义 (1)新建关系 (2)删除关系 四、SQL 的「数据查询」功能 (1)基本结构 ① Select 语句的基本结构 ② Select 子句 ③ Where 子句 ④ 空值的处…...
【小样本分割 2022 ECCV】SSP
文章目录 【小样本分割 2022 ECCV】SSP摘要1. 介绍2. 相关工作3. 自支持小样本语义分割3.1 动机3.2 自支持原型-SSM3.3 自适应自支持背景原型-ASBP3.4 自支持匹配-SSL 3. 代码 【小样本分割 2022 ECCV】SSP 论文题目:Self-Support Few-Shot Semantic Segmentation 中…...
Friendlycore增加inodes数量
背景:为Nanopim1安装了core系统,tf卡大小64G,安装后正常扩展到了整个tf卡,但是在安装hass的docker显示磁盘空间不够,最终发现是inode被用完了。其inode只有960K,但是16G卡树莓派系统的inodes都是其两倍。 一…...
Latex 定理和证明类环境(amsthm)和(ntheorm)的区别
最近在写毕业论文,出现了一些定理和证明的环境的问题,问题出现在对两个包的理解程度不够的问题上: \RequirePackage{ntheorem} 1、\newtheorem*{proof}{\hspace{2em}证:} 这个是让证明失去计数原则,该命令不能用于 amsthm 2…...
Yolov8改进---注意力全家桶,小目标涨点
💡💡💡💡💡💡💡💡💡💡注意力全家桶💡💡💡💡💡💡💡💡💡💡💡 基于Yolov8的注意力机制研究,提升小目标、遮挡物、难样本等检测性能...
[Linux]网络连接、资源共享
⭐作者介绍:大二本科网络工程专业在读,持续学习Java,输出优质文章 ⭐作者主页:逐梦苍穹 ⭐所属专栏:Linux基础操作。本文主要是分享一些Linux系统常用操作,内容主要来源是学校作业,分享出来的…...
来上海一个月的记录、思考和感悟
作者 | gongyouliu 编辑 | gongyouliu 从4月3号早上来上海,到今天差不多整整一个月了,也是自己正式从杭州离职创业(我更愿意称之为自由职业者,毕竟我没有招聘全职员工,有两个朋友业余时间在帮我)的第一个月…...
学校信息化管理系统通常包含哪些功能?
学校管理信息化是现代教育发展的必然趋势,随着信息技术的飞速发展,学校管理也逐渐地实现了信息化。信息化的学校管理已经成为教育现代化建设的重要内容,也是提高学校教育教学质量和保障学生安全的重要手段。 作为一款低代码开发平台…...
Java时间类(三) -- Calendar()(日历类)
java.util.Calendar类是一个抽象类,它提供了日期计算的相关功能、获取或设置各种日历字段的方法。 protected Calendar() 构造方法为protected修饰,无法直接创建该对象。1. Calendar()的常用方法: 方法名说明static Calendar getInstance()使用默认时区和区域获取日历vo…...
【五一创作】QML、Qt Quick /Qt中绘制圆形
目录标题 Qt Quick中绘制圆形扩展知识Canvas 模块介绍Shapes 模块介绍 Qt Widgets 中绘制圆形两种方式的比较 Qt Quick中绘制圆形 有多种方法可以在 Qt Quick 中绘制圆形。以下是一些主要方法: 使用 Canvas 元素 使用 Shapes 模块: a. 使用 PathArc 和…...
【软考数据库】第七章 关系数据库
目录 7.1 关系数据库概述 7.2 关系代数 7.3 元组演算与域演算 7.4 查询优化 7.5 关系数据库设计 7.6 模式分解 前言: 笔记来自《文老师软考数据库》教材精讲,精讲视频在b站,某宝都可以找到,个人感觉通俗易懂。 7.1 关系数据…...
《SpringBoot中间件设计与实战》第1章 什么是中间件
一、写在前面 在互联网应用初期,所有用于支撑系统建设的,框架结构、基础工具、业务逻辑、功能服务包括页面展示等,都是在一个系统中开发完成,最终也只是把系统和数据库部署在同一台服务器上。也就是大多数开发者入门所接触到的 “单体” 系统。 那为什么会有中间件这个玩…...
spring常用的事务传播行为
事务传播行为介绍 Spring中的7个事务传播行为: 事务行为 说明 PROPAGATION_REQUIRED 支持当前事务,假设当前没有事务。就新建一个事务 PROPAGATION_SUPPORTS 支持当前事务,假设当前没有事务,就以非事务方式运行 PROPAGATION_MANDATORY…...
【Python】什么是爬虫,爬虫实例
有s表示加密的访问方式 一、初识爬虫 什么是爬虫 网络爬虫,是一种按照一定规则,自动抓取互联网信息的程序或者脚本。由于互联网数据的多样性和资源的有限性,根据用户需求定向抓取相关网页并分析已成为如今主流的爬取策略爬虫可以做什么 你可以…...
JavaScript学习笔记(三)
文章目录 第7章:迭代器与生成器1. 迭代器模式2. 生成器 第8章:对象、类与面向对象编程1. 理解对象2. 创建对象3. 继承:依靠原型链实现4. 类class 第10章:函数1. 函数定义的方式有:函数声明、函数表达式、箭头函数&…...
文鼎创智能物联云原生容器化平台实践
作者:sekfung,深圳市文鼎创数据科技有限公司研发工程师,负责公司物联网终端平台的开发,稳定性建设,容器化上云工作,擅长使用 GO、Java 开发分布式系统,持续关注分布式,云原生等前沿技…...
深入了解SpringMVC框架,探究其优缺点、作用以及使用方法
一、什么是Spring MVC SpringMVC是一种基于Java的Web框架,与Spring框架紧密结合,用于开发具备WebApp特性的Java应用程序。Spring MVC是Spring Framework的一部分,因此它具有与Spring框架相同的特性和理念。 二、SpringMVC的优缺点 1. 优点…...
Git教程(一)
1、Git概述 1.1 、Git历史 同生活中的许多伟大事件一样,Git 诞生于一个极富纷争大举创新的年代。Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)…...
数据结构篇三:双向循环链表
文章目录 前言双向链表的结构功能的解析及实现1. 双向链表的创建2. 创建头节点(初始化)3. 创建新结点4. 尾插5. 尾删6. 头插7. 头删8. 查找9. 在pos位置前插入10. 删除pos位置的结点11. 销毁 代码实现1.ListNode.h2. ListNode.c3. test.c 总结 前言 前面…...
day10 TCP是如何实现可靠传输的
TCP最主要的特点 1、TCP是面向连接的运输层协议。( 每一条TCP连接只能有两个端点(endpoint),每一条TCP连接只能是点对点的(一对一)) 2、TCP提供可靠交付的服务。 3、TCP提供全双工通信。 4…...
Python | 人脸识别系统 — 背景模糊
本博客为人脸识别系统的背景模糊代码解释 人脸识别系统博客汇总:人脸识别系统-博客索引 项目GitHub地址:Su-Face-Recognition: A face recognition for user logining 注意:阅读本博客前请先参考以下博客 工具安装、环境配置:人脸…...
YOLOv5+单目测量物体尺寸(python)
YOLOv5单目测量尺寸(python) 1. 相关配置2. 测距原理3. 相机标定3.1:标定方法1(针对图片)3.2:标定方法2(针对视频) 4. 相机测距4.1 测距添加4.2 细节修改(可忽略…...
C++异常
C异常 提到异常,大家一定不陌生,在学习new关键字的时候就提到了开空间失败会导致抛异常。其实异常在我们生活中的使用是很多的,有些时候程序发生错误以后我们并不希望程序就直接退出,针对不同的情况,我们更希望有不同的…...
Java中的字符串是如何处理的?
Java中的字符串是通过字符串对象来处理的。字符串是一个类,可以创建一个字符串对象,并在该对象上调用一系列方法来操作该字符串。 Java中的字符串是不可变的,这意味着一旦创建了一个字符串对象,就无法修改它的值。任何对字符串对…...
【热门框架】怎样使用Mybatis-Plus制作标准的分页功能
使用 Mybatis-Plus 实现标准的分页功能需要使用 Page 类来进行分页操作。具体步骤如下: 引入 Mybatis-Plus 依赖 在 Maven 项目中,在 pom.xml 文件中引入 Mybatis-Plus 的依赖: <dependency><groupId>com.baomidou</groupId&g…...
用国外服务器做赌博网站/厦门网络推广外包
析 构 方 法 封装,有一个叫构造函数 和构造函数对应的还有一种方法叫做析构。 class ren //一个类 是 人类 { public $mingzi ;//成员变量 punction__destruct() //析构方法 { } } 析构方法,有什么做用??什么时…...
百度推广 网站建设/seo网站关键词优化价格
sql中的取模,取整,字符串连接等操作:c a mod b ;//取模c trunc(a/b);//取整//连接两个字符串,sql中不能用号连接两个字符串c a || b;或c concat(a,b);----------------------------------------------------------关于Oracle取整的函数分别有以下几种:1.取整(大) select ce…...
合肥简川科技网站建设公司 概况/百度推广登录平台登录
项目管理利器(Maven)——依赖范围(classPath:编译,运行,测试)1.compile:默认范围,编译测试运行都有效2.provided:在编译和测试时有效3.runtime:在…...
真正免费的网站建站平台b站/推广普通话作文
Swing概述 GUI——图形用户界面。该技术是目前最为常见的应用程序模型,如手机APP、Web网站等。 Java GUI最早使用的工具包是AWT(抽象窗口工具包),这个工具包提供了一套与本地图形界面交互的接口。AWT中的图形函数与操作系统所提…...
wordpress多图片/在线推广企业网站的方法有哪些
在这篇文章里讲述了历史数据的使用。在实际使用时,有时会发现历史数据有个边界问题。下面进行讲解, 一 问题 下面是带历史数据功能的server代码, #include <signal.h> #include <stdlib.h> #include <unistd.h>#include &…...
wordpress媒体默认链接/灰色行业推广平台网站
使用 <script setup>组合式 API 的语法糖的时候,defineProps报错: 代码如下: 第一次写vue3的项目,真的是到处都是坑啊,我就不断的百度百度再百度,发现再 module.exports {root: true,env: {node: …...