提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、线程池
1.1 池化(Pooling)
池化(Pooling)思想是一种常见的软件设计模式,主要目的是在程序运行过程中预先创建和维护一组可重用的资源对象(如线程(Thread)、数据库连接(Connection)等),以便在需要时能够快速获取,而不是每次需要时都动态创建新的对象。这种做法的核心在于,通过重复利用已经创建的对象,可以避免频繁的创建和销毁操作,从而提升系统的性能、资源利用率和响应速度。

1.2 为什么需要线程池
在Java中,创建一个线程需要执行new Thread()先获得一个线程对象,然后调用start()方法使得线程开始运行。对于在高并发场景下,如果频繁地创建对象,容易出现OOM(Out Of Memory)错误,并且,如果创建的线程数过多,频繁地进行线程上下文切换也会影响到程序性能。
在Java中使用线程池的主要目的是有效地管理和重用线程,从而提高应用程序的性能和资源利用率。具体来说,使用线程池有以下几个重要的原因:
1.减少资源消耗:创建和销毁线程是一项昂贵的操作,涉及到操作系统的资源分配和内存管理。如果频繁地创建和销毁线程,会消耗大量的系统资源,降低系统的性能。
2.提高响应速度:当任务到达时,线程池中的线程可以立即开始执行,而不需要等待新线程的创建。
3.提高系统稳定性:线程池能够限制并发线程的数量,防止系统因为过度并发而崩溃,提高了系统的稳定性。
4.方便管理线程:线程池提供了统一的接口和管理机制,可以方便地对线程进行监控、统计、调优等操作。
二、自定义线程池
我们可以简单地模拟手写一个基础的线程池实现。基本设计如下图所示:

其中,线程池从任务队列中取出任务Task交给内部维护的线程Thread来处理,扮演的是“消费者”的角色;任务队列用于存放需要被调度执行的任务,扮演的是“消费品”角色;main线程用于产生任务并将任务存放到任务队列中,扮演的是“生产者”的角色。
2.1 任务队列 - TaskBlockingQueue
任务队列是一个阻塞队列,其实现如下所示:
public class TaskBlockingQueue<T> {
/**
* 任务队列:存放任务集合
*/
private Deque<T> queue = new ArrayDeque<>();
/**
* 同步线程之间获取资源
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 生产者条件变量 => 满等待:任务队列queue已满,暂停新增任务
*/
private Condition fullWaitSet = lock.newCondition();
/**
* 消费者条件变量 => 空等待:任务队列queue为空,暂停消费任务
*/
private Condition emptyWaitSet = lock.newCondition();
/**
* 队列容量大小
*/
private int capacity;
public TaskBlockingQueue(int capacity) {
this.capacity = capacity;
}
/**
* 阻塞取出任务
*
* @return
*/
public T take() {
lock.lock();
try {
//当前任务资源为空 -> 暂停消费
while (queue.isEmpty()) {
//消费者等待
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出任务消费 --> 头部取出
T task = queue.removeFirst();
//唤醒满等待线程
fullWaitSet.signal();
return task;
} finally {
lock.unlock();
}
}
/**
* 阻塞添加任务
* @param task
*/
public void put(T task) {
lock.lock();
try {
//任务队列满 -- 生产者暂停添加任务
while (queue.size() == this.capacity) {
try {
//无时限等待
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以添加任务 -- 尾部添加
queue.addLast(task);
//唤醒空等待线程
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
}
2.2 线程池 - MyThreadPool
自定义线程池类实现如下:
public class MyThreadPool {
/**
* 任务队列
*/
private TaskBlockingQueue<Runnable> taskQueue;
/**
* 核心线程数
*/
private int coreSize;
/**
* 执行任务的线程集合
*/
private HashSet<Worker> workers = new HashSet<>();
/**
* 工作线程
*/
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//任务不为空时 -- 执行任务
while (null != task || (task = taskQueue.take()) != null) {
try {
log.debug("执行任务: {}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
//循环结束-任务执行完毕,移除worker线程
synchronized (this) {
log.debug("移除worker: {}", this);
workers.remove(this);
}
}
}
public MyThreadPool(int taskQueueSize, int coreSize) {
this.coreSize = coreSize;
this.taskQueue = new TaskBlockingQueue<>(taskQueueSize);
}
/**
* 线程池执行任务
* @param task
*/
public void execute(Runnable task) {
synchronized (workers) {
//直接交给Worker线程执行
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("创建Worker: {}, 任务: {}", worker, task);
workers.add(worker);
worker.start();
}
//加入阻塞队列暂存
else {
log.debug("新增等待任务: {}", task);
taskQueue.put(task);
}
}
}
}
2.3 测试自定义线程池
2.3.1 死等情况
假设自定义线程池的核心线程数为2,任务队列容量为10,编写测试类如下所示:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10, 2);
for (int i = 0; i < 5; i++) {
int j = i;
pool.execute(() -> {
log.debug("===> {} ", j + 1);
});
}
}
}
查看执行结果如下:

分析可知,工作的两个线程分别为Thread-0、Thread-1。初始时,工作线程数小于核心线程数,Thread-0、Thread-1得到创建并开始执行任务fcb0、3e64(用打印结果的最后4位来代指任务),在Thread-0、Thread-1工作期间,新增的任务37bc、a09c、0994暂时没有空闲的线程来执行,需要放入到任务队列中。当Thread-0、Thread-1执行完任务fcb0、3e64后变为空闲状态,然后从任务阻塞队列(按照加入顺序)读取任务37bc、a09c并执行,执行完毕后Thread-0、Thread-1又变回空闲线程,然后Thread-0取出任务0994执行,Thread-1为空闲且保持运行状态。Thread-0执行完任务0994后,同样处于空闲且保持运行状态。
在整个过程中,只创建了2个(核心线程数)个工作线程用于工作和复用,但从最后结果来看,当任务队列为空即工作线程已经没有任务要执行时,却依旧处于运行状态以等待下一个任务,故需要加上一个超时时间设置来避免工作线程死等的情况。
2.3.2 超时时间设置
对任务阻塞队列的take()方法做增加超时时间设置的改造,如下所示:
/**
* 阻塞取出任务 -- 设置超时时间
*
* @return
*/
public T poll(long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
//当前任务资源为空 -> 暂停消费
while (queue.isEmpty()) {
//消费者等待
try {
//超时时间到
if (nanos <= 0) {
return null;
}
//剩余时间
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出任务消费 --> 头部取出
T task = queue.removeFirst();
//唤醒满等待线程
fullWaitSet.signal();
return task;
} finally {
lock.unlock();
}
}
同样,线程池实现也做一个对应改造:
public class MyThreadPool {
/**
* 任务队列
*/
private TaskBlockingQueue<Runnable> taskQueue;
/**
* 核心线程数
*/
private int coreSize;
/**
* 执行任务的线程集合
*/
private HashSet<Worker> workers = new HashSet<>();
/**
* 时间单位
*/
private TimeUnit timeUnit;
/**
* 超时时间
*/
private long timeout;
/**
* 工作线程
*/
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//任务不为空时 -- 执行任务
while (null != task || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("执行任务: {}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
//循环结束,移除worker线程
synchronized (this) {
log.debug("移除worker: {}", this);
workers.remove(this);
}
}
}
public MyThreadPool(int taskQueueSize, int coreSize, TimeUnit timeUnit, long timeout) {
this.coreSize = coreSize;
this.taskQueue = new TaskBlockingQueue<>(taskQueueSize);
this.timeUnit = timeUnit;
this.timeout = timeout;
}
/**
* 线程池执行任务
* @param task
*/
public void execute(Runnable task) {
synchronized (workers) {
//直接交给Worker线程执行
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("创建Worker: {}, 任务: {}", worker, task);
workers.add(worker);
worker.start();
}
//加入阻塞队列暂存
else {
log.debug("新增等待任务: {}", task);
taskQueue.put(task);
}
}
}
}
假设线程池的超时时间设置为2秒,编写测试类如下,
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10, 2, TimeUnit.SECONDS, 2);
for (int i = 0; i < 5; i++) {
int j = i;
pool.execute(() -> log.debug("===> {} ", j + 1));
}
}
}
运行结果如下所示:

可以看到,超时时间到了以后,工作线程被移除,整个程序运行结束,工作线程不再是无限死等。
2.3.3 任务队列满
现假设需要执行的任务数暴增,从5个变为15个,已知任务队列的大小为10。编写测试类如下:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10, 2, TimeUnit.SECONDS, 2);
for (int i = 0; i < 15; i++) {
int j = i;
pool.execute(() -> log.debug("===> {} ", j + 1));
}
}
}
运行结果如下:

向任务队列添加任务时都是阻塞式地添加,在任务数量过多的情况下,如果主线程需要执行过多的添加任务操作,那么就会对主线程的性能造成严重影响,所以对于核心工作线程无空闲的且阻塞任务队列又已满的情况,应该让主线程可以选择性地执行一些拒绝策略。
三、 自定义拒绝策略
3.1 带超时添加任务
改造任务阻塞队列的添加任务方法put,新增超时时间判断处理,如下所示:
/**
* 阻塞添加任务 -- 新增超时时间设置
* @param task
*/
public boolean offer(T task, long timeout, TimeUnit unit) {
lock.lock();
try {
long nanos = unit.toNanos(timeout);
//任务队列满 -- 生产者暂停添加任务
while (queue.size() == this.capacity) {
log.debug("等待加入任务队列: {}", task);
if (nanos <= 0) {
return false;
}
try {
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以添加任务 -- 尾部添加
log.debug("加入任务队列: {}", task);
queue.addLast(task);
//唤醒空等待线程
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
3.2 拒绝方法
为方便灵活设置拒绝策略,可将拒绝策略定义为一个函数式接口,如下所示:
@FunctionalInterface
public interface MyRejectPolicy<T> {
/**
* 拒绝策略设置
* @param taskQueue
* @param task
*/
void reject(TaskBlockingQueue<T> taskQueue, T task);
}
改造任务阻塞队列,新增拒绝方法如下所示:
/**
* 尝试向任务队列新增任务
* @param rejectPolicy
* @param task
*/
public void tryPut(MyRejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//队列已满 ==> 执行拒绝策略
if (queue.size() == capacity) {
log.debug("队列已满,执行拒绝策略...");
rejectPolicy.reject(this, task);
} else {
//队列还有空闲
log.debug("加入任务队列: {}", task);
queue.addLast(task);
//唤醒空等待
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
自定义线程池实现类改造如下:
public class MyThreadPool {
/**
* 任务队列
*/
private TaskBlockingQueue<Runnable> taskQueue;
/**
* 拒绝策略
*/
private MyRejectPolicy<Runnable> rejectPolicy;
/**
* 核心线程数
*/
private int coreSize;
/**
* 执行任务的线程集合
*/
private HashSet<Worker> workers = new HashSet<>();
/**
* 时间单位
*/
private TimeUnit timeUnit;
/**
* 超时时间
*/
private long timeout;
/**
* 工作线程
*/
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
//任务不为空时 -- 执行任务
while (null != task || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("执行任务: {}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
//循环结束,移除worker线程
synchronized (this) {
log.debug("移除worker: {}", this);
workers.remove(this);
}
}
}
public MyThreadPool(int taskQueueSize, int coreSize, TimeUnit timeUnit, long timeout,
MyRejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.taskQueue = new TaskBlockingQueue<>(taskQueueSize);
this.timeUnit = timeUnit;
this.timeout = timeout;
this.rejectPolicy = rejectPolicy;
}
/**
* 线程池执行任务
* @param task
*/
public void execute(Runnable task) {
synchronized (workers) {
//直接交给Worker线程执行
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("创建Worker: {}, 任务: {}", worker, task);
workers.add(worker);
worker.start();
}
//加入阻塞队列暂存 -- 设置拒绝策略
else {
taskQueue.tryPut(rejectPolicy, task);
}
}
}
}
3.3 测试自定义拒绝策略
3.3.1 拒绝策略-死等
当前工核心线程数为2,任务队列容量为2,待处理任务数量为8,测试类如下所示:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(2, 2, TimeUnit.MILLISECONDS, 1000,
TaskBlockingQueue::put);
for (int i = 0; i < 8; i++) {
int j = i;
pool.execute(() -> {
try {
//假设工作线程耗时特别久
TimeUnit.MILLISECONDS.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下所示:

在死等的拒绝策略下,当工作线程无空闲且任务队列已满时,新增任务时将会一直等待到任务队列有空闲为止。
3.3.2 有时限等待 - 超时时间之内
现假设任务执行耗时为2s,超时等待时间为2s,拒绝策略为在1s内尝试将新任务加入任务队列,测试类如下所示:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(1, 1, TimeUnit.MILLISECONDS, 2000,
(queue, task) -> {
//在1s内尝试将任务加到任务队列
queue.offer(task, 1000, TimeUnit.MILLISECONDS);
});
for (int i = 0; i < 4; i++) {
int j = i;
pool.execute(() -> {
try {
//工作线程执行耗时为2s
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下:

执行拒绝策略时,在超时时间1s内尝试将新任务加入任务队列,但是都加入失败,故最后4个任务中只成功执行了两个任务,另外两个任务因加入任务队列失败而执行失败。
3.3.3 有时限等待 - 超时时间之外
现假设任务执行耗时为2s,超时等待时间为2s,拒绝策略为在3s内尝试将新任务加入任务队列,测试类如下所示:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(1, 1, TimeUnit.MILLISECONDS, 2000,
(queue, task) -> {
//在3s内尝试将任务加到任务队列
queue.offer(task, 3000, TimeUnit.MILLISECONDS);
});
for (int i = 0; i < 4; i++) {
int j = i;
pool.execute(() -> {
try {
//工作线程执行耗时为2s
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下所示:

工作线程执行任务耗时为2s,而执行拒绝策略时,新增任务的超时等待时间为3s,则新任务可正常被加入到任务队列中,故4个任务全部被执行成功。
3.3.4 放弃任务执行
假设当前核心线程数和任务队列数都为1,待执行任务数量为4,测试类代码如下所示:
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(1, 1, TimeUnit.MILLISECONDS, 2000,
(queue, task) -> {
log.debug("放弃执行: {}", task);
});
for (int i = 0; i < 4; i++) {
int j = i;
pool.execute(() -> {
try {
//工作线程执行耗时为2s
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下所示:

当任务队列满以后,执行拒绝策略时,最后两个任务放弃了执行,故最终只有两个任务可被正常执行。
3.3.5 抛异常
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(1, 1, TimeUnit.MILLISECONDS, 2000,
(queue, task) -> {
throw new RuntimeException("任务执行失败...");
});
for (int i = 0; i < 4; i++) {
int j = i;
pool.execute(() -> {
try {
//工作线程执行耗时为2s
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下所示:

执行拒绝策略时,当新任务无法加入到任务队列时,将直接抛出异常。
3.3.6 调用者线程执行
public class TestMyPool {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(1, 1, TimeUnit.MILLISECONDS, 2000,
(queue, task) -> {
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
pool.execute(() -> {
try {
//工作线程执行耗时为2s
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("===> {} ", j + 1);
});
}
}
}
运行结果如下所示:

执行拒绝策略时,无法加入到任务队列的任务将交由调用者线程即main线程来执行,所有任务都可以被执行成功,但是负责执行的线程不一样。
自定义线程池与拒绝策略
2594

被折叠的 条评论
为什么被折叠?



