设计线程池的原因
所以尽可能的重用已有的线程去处理任务。
阻塞队列:放任务的,线程池暂时处理不了的任务先放到阻塞队列里来;线程池能处理了,再从阻塞队列中去取任务执行
// 注意一下这个ReentrantLock的从条件变量中唤醒,之前没有正确理解
// 现在的理解是: 从ReentrantLock的条件变量唤醒之后,跟synchronized从waitSet中唤醒一样,要进入entryList中等待获取锁,
// 获取锁成功之后, 才能运行, 不然你以为, 最后的那个释放锁是怎么释放的
@Slf4j(topic = "c.BlockingQueue")
public class BlockingQueue<T> {
// 任务队列
private ArrayDeque<T> queue = new ArrayDeque<>();
// 锁
private ReentrantLock lock = new ReentrantLock();
// 生产者条件变量 (任务队列满了的时候, 等待)
Condition fullWaitSet = lock.newCondition();
// 消费者条件变量 (任务队列为空的时候, 等待)
Condition emptyWaitSet = lock.newCondition();
// 任务队列最大容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);
lock.lock();
try {
while (queue.isEmpty()) {
if (nanos <= 0) {
// nanos小于或等于0, 就代表不需要等待了
return null;
}
try {
nanos = emptyWaitSet.awaitNanos(nanos); // 返回值是nanos减去等待的时间(即还需等待的时间)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal(); // 通知因为满了而等待的线程(因为满了而等待的线程肯定是等着往队列里面添加任务嘛)
return t;
} finally {
lock.unlock(); // 这里才释放的锁
}
}
// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
// 当任务队列中没有任务时,就等待
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal(); // 唤醒在fullWaitSet中等待的随机一个线程,但这个被唤醒的线程并不是马上运行,
return t; // 而是要进入entryList中等待获取锁,锁被释放了,它才能去竞争锁(注意一下,这里还并未释放锁哦)
} finally {
// 下面的这个put方法在lock.lock()这句,在take方法里获取锁但未释放前,外面线程进不来,
lock.unlock();// 里面的线程又在entryList中等待获取锁,所以保证了安全
} // 这里才释放的锁
}
// 阻塞添加
public void put(T element) {
lock.lock();
try {
while (queue.size() == capacity) {
// 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
try {
log.debug("任务队列已满,等待加入任务队列: {}",element);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列: {}",element);
queue.addLast(element);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
}
线程池
@Slf4j(topic = "c.ThreadPool")
public class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 工作线程集合
HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
// 超时时间的时间单位
private TimeUnit timeUnit;
// 执行任务:
// 如果当前的线程数还没有超过核心线程数,那么就创建一个线程去执行这个提交的任务
// 如果当前的线程数已经超过了核心线程数,那么就把这个任务放到BlockingQueue中
public void execute(Runnable task) {
synchronized (workers) {
// 这里既包含对worker的读,也包含对worker的写,所以需要保护
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("创建线程: {}, 关联任务: {}",worker,task);
workers.add(worker);
worker.start();
} else {
taskQueue.put(task);
}
}
}
// 构造一个自定义线程池对象
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit,int queueCapacity) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapacity);
}
class Worker extends Thread{
// 执行的任务
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
// Thread#start 方法会开启线程执行线程线程对象的run方法,所以这里重写run方法
@Override
public void run() {
// 自己的任务执行完了的话,再看下任务队列里面还有没有任务,没想到里面是个坑,哈哈
// while (task != null || (task = taskQueue.take())!=null) { // 死等
// 没有任务就等待,有任务就返回,感觉这个线程就是个工具人,除非时调用的是超时获取的方法
while (task != null || (task = taskQueue.poll(timeout,timeUnit))!=null) {
try {
log.debug("执行任务...{}",task);
task.run();
} finally {
task = null; // task执行完了的话,就置为null
}
}
synchronized (workers) {
workers.remove(this);
log.debug("移除线程: {}",this);
}
}
}
}
测试
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
for (int i = 0; i < 5; i++) {
final int j = i;
pool.execute(()->{
log.debug("{}",j);
});
}
}
}
测试结果
17:37:44.339 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:37:44.342 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:37:44.342 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:37:44.342 [Thread-0] DEBUG c.TestPool - 0
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:37:44.342 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
17:37:44.342 [Thread-1] DEBUG c.TestPool - 1
17:37:44.342 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:37:44.343 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@38997010
17:37:44.343 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@1942406066
17:37:44.343 [Thread-0] DEBUG c.TestPool - 2
17:37:44.343 [Thread-1] DEBUG c.TestPool - 3
17:37:44.343 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1213415012
17:37:44.343 [Thread-0] DEBUG c.TestPool - 4
17:37:45.343 [Thread-1] DEBUG c.ThreadPool - 移除线程: Thread[Thread-1,5,main]
17:37:45.343 [Thread-0] DEBUG c.ThreadPool - 移除线程: Thread[Thread-0,5,main]
但是上面存在问题:假设如果main线程提交的任务,超过了任务队列的大小,并且核心线程也处理不过来,那么main线程就被阻塞住了,测试如下。
并且我们可以看到,main线程本来要提交15个线程,但是只有2个正在被执行,10个添加到了任务队列,还剩3个没有着落,main线程就被阻塞住了。
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
for (int i = 0; i < 15; i++) {
final int j = i;
pool.execute(()->{
try {
Thread.sleep(1000000L); // 模拟处理时长
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",j);
});
}
}
}
/*测试结果*/ // 程序没有结束,/*
17:43:55.790 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:43:55.795 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1688376486
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@2114664380
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@999661724
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1793329556
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@445884362
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1031980531
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@721748895
17:43:55.797 [main] DEBUG c.BlockingQueue - 任务队列已满,等待加入任务队列: element@1642534850
17:43:55.798 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:43:55.799 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
*/
但是上面存在问题:假设如果main线程提交的任务,超过了任务队列的大小,并且核心线程也处理不过来,那么main线程就被阻塞住了,测试如下。
并且我们可以看到,main线程本来要提交15个线程,但是只有2个正在被执行,10个添加到了任务队列,还剩3个没有着落,main线程就被阻塞住了,可以看到main线程现在控制不了这个局面了,所以线程池还应该设计成让main线程可以选择遇到这种情况下应该要怎么做,是死等?让调用者放弃任务?让调用者超时等待?让调用者抛出异常?
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
for (int i = 0; i < 15; i++) {
final int j = i;
pool.execute(()->{
try {
Thread.sleep(1000000L); // 模拟处理时长
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",j);
});
}
}
}
/*测试结果*/ // 程序没有结束,/*
17:43:55.790 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-0,5,main], 关联任务: task@1323468230
17:43:55.795 [main] DEBUG c.ThreadPool - 创建线程: Thread[Thread-1,5,main], 关联任务: task@897697267
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@38997010
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1942406066
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1213415012
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1688376486
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@2114664380
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@999661724
17:43:55.796 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1793329556
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@445884362
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@1031980531
17:43:55.797 [main] DEBUG c.BlockingQueue - 加入任务队列: element@721748895
17:43:55.797 [main] DEBUG c.BlockingQueue - 任务队列已满,等待加入任务队列: element@1642534850
17:43:55.798 [Thread-0] DEBUG c.ThreadPool - 执行任务...task@1323468230
17:43:55.799 [Thread-1] DEBUG c.ThreadPool - 执行任务...task@897697267
*/
因此抽象出一个拒绝策略接口,把任务队列满了时的代码控制权交给main线程决定
其实跟上面比较,改动的并不多,主要是:当核心线程满了的时候的逻辑,当还有任务提交时,就把任务继续交给任务队列,而在任务队列里面添加一个逻辑:当任务队列满了的时候,就执行拒绝策略,拒绝策略回传的参数就是当前的队列this和当前提交的任务,把这两个参数交给线程池的属性拒绝策略对象的reject方法处理。
public interface RejectPolicy<T> {
void reject(BlockingQueue<T> blockingQueue, T task);
}
// 注意一下这个ReentrantLock的从条件变量中唤醒,之前没有正确理解
// 现在的理解是: 从ReentrantLock的条件变量唤醒之后,跟synchronized从waitSet中唤醒一样,要进入entryList中等待获取锁,
// 获取锁成功之后, 才能运行, 不然你以为, 最后的那个释放锁是怎么释放的
@Slf4j(topic = "c.BlockingQueue")
public class BlockingQueue<T> {
// 任务队列
private ArrayDeque<T> queue = new ArrayDeque<>();
// 锁
private ReentrantLock lock = new ReentrantLock();
// 生产者条件变量 (任务队列满了的时候, 等待)
Condition fullWaitSet = lock.newCondition();
// 消费者条件变量 (任务队列为空的时候, 等待)
Condition emptyWaitSet = lock.newCondition();
// 任务队列最大容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
if (nanos <= 0) {
// nanos小于或等于0, 就代表不需要等待了
return null;
}
try {
nanos = emptyWaitSet.awaitNanos(nanos); // 返回值是nanos减去等待的时间(即还需等待的时间)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal(); // 通知因为满了而等待的线程(因为满了而等待的线程肯定是等着往队列里面添加任务嘛)
return t;
} finally {
lock.unlock(); // 这里才释放的锁
}
}
// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
// 当任务队列中没有任务时,就等待
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal(); // 唤醒在fullWaitSet中等待的随机一个线程,但这个被唤醒的线程并不是马上运行,
return t; // 而是要进入entryList中等待获取锁,锁被释放了,它才能去竞争锁(注意一下,这里还并未释放锁哦)
} finally {
// 下面的这个put方法在lock.lock()这句,在take方法里获取锁但未释放前,外面线程进不来,
lock.unlock();// 里面的线程又在entryList中等待获取锁,所以保证了安全
} // 这里才释放的锁
}
// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capacity) {
// 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
try {
log.debug("任务队列已满,等待加入任务队列: {}", "task@" + task.hashCode());
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列: {}", "element@" + task.hashCode());
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capacity) {
// 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
try {
if (nanos <= 0) {
return false;
}
log.debug("任务队列已满,等待加入任务队列: {}", "task@" + task.hashCode());
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列: {}", "element@" + task.hashCode());
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
// 在上面原本的put方法上, 引入拒绝策略接口
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
if (queue.size() == capacity) {
// 当任务队列满时, 就等待(此时,因为不能添加任务了嘛)
log.debug("任务队列已满,执行拒绝策略...{}", "task@" + task.hashCode());
rejectPolicy.reject(this, task);
} else {
log.debug("加入任务队列: {}", "element@" + task.hashCode());
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}
@Slf4j(topic = "c.ThreadPool")
public class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 工作线程集合
HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
// 超时时间的时间单位
private TimeUnit timeUnit;
private RejectPolicy rejectPolicy;
// 执行任务:
// 如果当前的线程数还没有超过核心线程数,那么就创建一个线程去执行这个提交的任务
// 如果当前的线程数已经超过了核心线程数,那么就把这个任务放到BlockingQueue中
public void execute(Runnable task) {
synchronized (workers) {
// 这里既包含对worker的读,也包含对worker的写,所以需要保护
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("创建线程: {}, 关联任务: {}",worker,"task@"+task.hashCode());
workers.add(worker);
worker.start();
} else {
// taskQueue.put(task); // 死等 (原来是死等,现在改成带有拒绝策略的做法)
// 代码运行到这里,提交的任务已经超过了核心线程数,此时考虑到两种情况
// 1. 提交的任务还可以放到阻塞队列中
// 2. 阻塞队列如果也满了,需要把代码的控制权返还到调用者,让调用者决定采取什么方式?
// 1. 死等 2.超时等 3.放弃任务 4.抛出异常 ...
// 所以这里,抽象出一个接口出来RejectPolicy,让调用者在创建线程池时,传入
// 因此,这里的拒绝策略是当前线程池的一个属性,
// 而如果在提交的任务已经超过了核心线程数的情况下,把接下来的运行交给任务队列去做
taskQueue.tryPut(rejectPolicy,task);
}
}
}
// 构造一个自定义线程池对象
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit,
int queueCapacity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
// 执行的任务
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
// Thread#start 方法会开启线程执行线程线程对象的run方法,所以这里重写run方法
@Override
public void run() {
// 自己的任务执行完了的话,再看下任务队列里面还有没有任务,没想到里面是个坑,哈哈
// while (task != null || (task = taskQueue.take())!=null) { // 死等
// 没有任务就等待,有任务就返回,感觉这个线程就是个工具人
while (task != null || (task = taskQueue.poll(timeout,timeUnit))!=null) {
try {
// 除非时调用的是超时获取的方法
log.debug("执行任务...{}","task@"+task.hashCode());
task.run();
} finally {
task = null; // task执行完了的话,就置为null
}
}
synchronized (workers) {
workers.remove(this);
log.debug("移除线程: {}",this);
}
}
}
}
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10,
(queue, task) -> {
// 1.死等
// queue.put(task);
// 2.超时等待
// boolean flag = queue.offer(task, 500, TimeUnit.MILLISECONDS);
// log.debug("添加任务结果: {}", flag);
// 3.让调用者放弃任务
// log.debug("什么都不干,直接放弃任务...");
// 4.抛出异常, 当然此时,主线程后面添加的任务都没了
// throw new RuntimeException("任务队列满了");
// 5.主线程自己做
// task.run();
}
);
for (int i = 0; i < 15; i++) {
final int j = i;
pool.execute(() -> {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}