如何在不使用JDK的BlockingQueue
的情况下,手写实现阻塞队列的功能?可以使用ArrayList
或者LinkedList
。
队列比较好理解,数据结构中我们都接触过,是一种先进先出的数据结构,那什么是阻塞队列呢?从名字可以看出阻塞队列其实也就是队列的一种特殊情况,在队列的基础上做了些附加操作。
阻塞队列(BlockingQueue)是一种支持两个附加操作的队列。这两个附加操作是:
当队列为空时,获取元素的线程会阻塞等待,直到队列变为非空。
当队列为满时,存储元素的线程会阻塞等待,直到队列变为非满。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
方法 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add | offer | put | Offer(time) |
移除方法 | remove | poll | take | Poll(time) |
检查方法 | element | peek | N/A | N/A |
是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】
一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。
一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。
(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)
一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
ArrayBlockingQueue是基于Lock和Condition实现的。
/**
* 手写实现阻塞队列
*
* ReentrantLock与Condition结合使用
*/
public class MyArrayBlockingQueue<T> {
private volatile ArrayList<T> arrayList = new ArrayList<T>();
private ReentrantLock lock = new ReentrantLock();
//定义两个条件,分别为“队列非满”和“队列非空”,队列非满时才能新增数据,队列非空时才能读取数据
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
private volatile int capacity = 5;//阻塞队列的容量
private volatile int length = 0;//阻塞队列存储的数据个数
public static void main(String[] agrs) {
//testMyArrayBlockingQueue();
testMyArrayBlockingQueue1();
}
public static void testArrayBlockingQueue() {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(10);
}
public static void testMyArrayBlockingQueue() {
MyArrayBlockingQueue<Integer> myArrayBlockingQueue = new MyArrayBlockingQueue<>();
for (int i = 0; i < 10; ++i) {
final int data = i;
new Thread(new Runnable() {
@Override
public void run() {
myArrayBlockingQueue.put(data);
}
}).start();
}
try {
System.out.println("sleep");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Integer t = myArrayBlockingQueue.take();
}
}
}).start();
}
public static void testMyArrayBlockingQueue1() {
MyArrayBlockingQueue<Integer> myArrayBlockingQueue = new MyArrayBlockingQueue<>();
for (int i = 0; i < 10; ++i) {
final int data = i;
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
myArrayBlockingQueue.put(data);
}
}
}).start();
}
try {
System.out.println("sleep");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Integer t = myArrayBlockingQueue.take();
}
}
}).start();
}
/**
* 当一个线程调用put方法添加元素时,若集合满了则使调用线程阻塞,直到有其他线程从集合中take出数据。
* @param data
*/
public void put(T data) {
System.out.println("---put(), thread=" + Thread.currentThread().getName());
try {
//一旦一个线程封锁了锁对象, 其他任何线程都无法通过 lock 语句。
// 当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。
lock.lock();
// if(length == allow)
while (length == capacity) {//即队列非满时才能新增数据
//使调用线程挂起
//await()的作用是挂起当前线程,释放竞争资源的所,从而能够让其他线程访问竞争资源。
//当外部条件改变时,意味着某个任务可以继续执行,可以通过signal()或者signalAll()通知这个任务
System.out.println("put(), thread=" + Thread.currentThread().getName() + ",队列存储已经满了,挂起线程...");
notFullCondition.await();//condition的作用是使线程挂起,当外部满足某一条件时,再通过条件对象的signal()或者signalAll()方法唤醒等待的线程。
}
System.out.println("put(), thread=" + Thread.currentThread().getName() + ", 新增数据 data=" + data);
arrayList.add(data);
++length;
System.out.println("put(), thread=" + Thread.currentThread().getName() + ",唤醒等待着的读取数据的线程");
notEmptyCondition.signalAll();//唤醒所有在该条件上等待着的线程,即唤醒等待着的读取数据的线程
} catch (Exception e) {
e.printStackTrace();
} finally {
//每个lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都会释放锁,
//任务在调用await(),signal(),signalAll()之前必须拥有这个锁,即必须先调用lock()方法。
lock.unlock();
}
}
/**
* 当一个线程调用take方法获取元素时,若集合为空则使调用线程阻塞,直到有其他线程在集合中加入新元素。
* @return
*/
public T take() {
System.out.println("---take(), thread=" + Thread.currentThread().getName());
try {
lock.lock();
// if(length == 0)
while (length == 0) {//即队列非空时才能读取数据
System.out.println("take(), thread=" + Thread.currentThread().getName() + ",队列已没有数据,挂起线程...");
notEmptyCondition.await();
}
T data = arrayList.remove(0);
System.out.println("take(), thread=" + Thread.currentThread().getName() + ", 读取数据 data=" + data);
--length;
System.out.println("take(), thread=" + Thread.currentThread().getName() + ",唤醒等待着的新增数据的线程");
notFullCondition.signalAll();//唤醒所有在该条件上等待着的线程,即唤醒等待着的新增数据的线程
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
lock.unlock();
}
}
}
参考:
Java 阻塞队列–BlockingQueue
并发编程— 并发容器(除ConcurrentHashMap )与阻塞队列
java 手写阻塞队列_详解java中一个面试常问的知识点-阻塞队列
手写阻塞队列
Java技术——ReentrantLock的Condition的作用以及使用
Condition的await()和signal()流程