此外,通过设置有界队列,我们还可以限制队列的最大容量,防止任务无限堆积导致内存溢出。当队列已满时,我们可以采取拒绝策略(如直接丢弃、暂存等)来应对新加入的任务。
如下图,任务等待队列其实就是联系任务生成者与任务消费者的一个桥梁,生产者生产消息放入等待队列中,再由消费者拿取消费。
实现思路
为了实现一个功能完备的任务等待队列,我们需要设计一个阻塞队列BlockQueue,它具有以下几个关键特性:
- 有界队列 BlockQueue将设置一个固定的容量size,队列中最多只能存储size个任务。这样可以防止任务无限制地堆积,导致内存溢出。当队列满时,新添加的任务将被阻塞,直到队列中有空位。
- 线程安全 BlockQueue的操作,包括添加任务put和获取任务take,都需要保证线程安全。我们将使用Java的重入锁ReentrantLock和条件变量Condition来实现线程的正确同步。
- 支持超时 在添加或获取任务时,BlockQueue将提供带超时时间的方法offer和poll。这样可以防止任务被无限期阻塞,提高系统的响应性和健壮性。
基于以上思路,BlockQueue的实现将涉及以下几个核心部分:
- 使用双端队列Deque作为底层数据结构存储任务
- 使用ReentrantLock和两个Condition(emptyCondition和fullCondition)来实现阻塞和唤醒机制
- 实现put、offer、take、poll等核心方法,正确控制任务的添加和获取
代码实现
1.新建BlockQueue类
我们这里新建一个名为BlockQueue的类,并声明一些属性与字段,其中ArrayDeque为一个双端队列,可以在队列的两端操作元素,size为定义的队列大小
public class BlockQueue<T> {
// 双端队列
private Deque<T> deque = new ArrayDeque<>();
// 队列的容量
private int size;
public BlockQueue(int size) {
this.size = size;
}
}
2.任务的添加和获取方法
接下来我们编写向队列中添加任务与获取任务的方法,但是这里要注意的是,读写操作都是在线程池多线程的环境下进行的,存在线程安全问题,所以我们需要配合锁机制来保证操作的原子性。我们这里用JDK提供的ReentrantLock可重入锁来解决这个问题。
继续新增ReentrantLock字段:
private ReentrantLock lock = new ReentrantLock();
此外,我们还需要解决任务添加与获取时的等待操作,也就是当队列为空时,消费者需要等待任务产生,当队列满时,生产者需要等待队列中有空位才能存入任务。如下图,当消费者获取到锁时,会尝试获取任务,但发现队列为空,就会阻塞等待。
当生产者生产任务后,肯定不能让消费者干等着,而是去通知消费者有活做啦~如下图:
我们可以看到这里涉及到了生产者与消费者不同线程的通讯,这里我们可以借助Condition来完成消费者线程与生产者线程之间的通讯。
分别定义两个Condition代表队列满情况的等待室与队列空情况的等待室
// 队列空情况的休息室
Condition emptyCondition = lock.newCondition();
// 队列满情况的休息室
Condition fullCondition = lock.newCondition();
定义阻塞添加任务方法put
// 添加任务 阻塞添加
public void put(T task) {
lock.lock();
try {
while (size == deque.size()) {
try {
fullCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("task 添加成功 ,{}", task);
deque.addLast(task);
emptyCondition.signal();
} finally {
lock.unlock();
}
}
这里添加任务会存在两张情况:
- 队列满了:调用fullCondition.await()方法挂起当前生产者线程,也就是让当前生产者线程等待。
- 队列没满:将任务加入队列中,并调用emptyCondition.signal()通知挂起的消费者。
编写堵塞拿取任务方法take
// 获取任务
public T take() {
lock.lock();
try {
while (deque.isEmpty()) {
try {
emptyCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
T t = deque.removeFirst();
fullCondition.signal();
log.debug("获取了任务 {}", t);
return t;
} finally {
lock.unlock();
}
}
同理,这里拿取任务也会存在两张情况:
- 队列空的:调用emptyCondition.await()方法挂起当前消费者线程,也就是让当前消费者线程等待。
- 队列存在元素:任务出栈,并返回出栈的任务元素,然后调用fullCondition.signal()通知挂起的生产者。
以上我们其实我们就完成了一个简单的任务堵塞队列,但是我们会发现,如果这两个方法都是会一直堵塞,显然是不合理的,所以我们这里新增添加和获取任务的超时方法。
带超时时间的阻塞添加方法offer
// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (deque.size() == size) {
try {
if (nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullCondition.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
deque.addLast(task);
emptyCondition.signal();
return true;
} finally {
lock.unlock();
}
}
我们可以利用fullCondition.awaitNanos来实现超时等待,当超过给定参数时间时,就会被自动唤醒,并且将传入时间-等待时间作为返回值,下一次循环nanos <= 0时,就会判定为本次操作超时失败了
带超时时间的阻塞获取方法poll
// 带超时时间阻塞获取
public T poll(long timeout, TimeUnit unit) {
// 1.上锁
lock.lock();
try {
long nanos = unit.toNanos(timeout); // 转为毫秒
// 2.首先检查队列是否存在元素
while(deque.isEmpty()){
try {
// 2.1超时判断,返回值是剩余时间
if(nanos <= 0){
return null;
}
// 2.2超时等待
## 学习路线:
这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
![在这里插入图片描述](https://img-blog.csdnimg.cn/7a04c5d629f1415a9e35662316578e07.png#pic_center)