提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、线程池
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线程来执行,所有任务都可以被执行成功,但是负责执行的线程不一样。