前言
Java的并发编程中常常需要在多个线程之间共享数据。这时,线程之间的协调和通信就变得至关重要。而BlockingQueue就是Java中解决线程间通信问题的一种非常有用的工具。它是一个支持两个附加操作的队列:插入和删除。其中,插入操作在队列满时会阻塞等待空间变得可用,而删除操作在队列为空时会阻塞等待有数据可用。这种阻塞特性可以避免一些传统的线程通信问题,如死锁和饥饿等,因此广泛应用于Java的并发编程中。
阻塞队列介绍
队列数据结构
队列是一种线性数据结构,它可以用来存储和管理一组数据元素。队列的特点是先进先出(First-In-First-Out, FIFO),也就是说,最先进入队列的元素最先被处理,而最后进入队列的元素最后被处理。队列可以用于许多实际应用,如任务调度、消息传递、缓存等。
队列通常包含两个指针,一个指向队列的头部(Head),另一个指向队列的尾部(Tail)。新元素可以通过尾部插入队列(入队),而最老的元素可以通过头部删除队列(出队)。
阻塞队列
BlockingQueue是Java中的一个接口,它表示一个阻塞队列,即在队列为空时从队列中获取元素的操作将被阻塞,直到队列中有可用元素为止;而在队列已满时添加元素的操作将被阻塞,直到队列中有空闲位置为止。
BlockingQueue接口扩展了Queue接口,添加了一些阻塞操作,包括put和take方法。put方法将一个元素放入队列中,如果队列已满,则阻塞直到队列中有空闲位置为止。take方法从队列中获取一个元素,如果队列为空,则阻塞直到队列中有可用元素为止。除了put和take方法,BlockingQueue还有其他的操作,例如offer、poll、peek等等,它们与Queue接口中的操作类似,但是在队列已满或为空时的行为不同。
ArrayBlockingQueue
ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定容量大小,利用 ReentrantLock 实现线程安全。ArrayBlockingQueue可以用于实现数据缓存、限流、生产者-消费者模式等各种应用。在生产者-消费者模型中使用时,如果生产速度和消费速度基本匹配的情况下,使用ArrayBlockingQueue是个不错选择;如果生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞。
使用
public class ArrayBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个容量为3的阻塞队列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
new Thread(()->{
// 向队列中添加元素
try {
queue.put("a");
queue.put("b");
queue.put("c");
System.out.println("队列中当前元素个数:" + queue.size());
// 尝试向队列中添加一个元素,但是队列已满,将会被阻塞
queue.put("d");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(500);
// 从队列中获取元素
String element = queue.take();
System.out.println("获取到的元素为:" + element);
System.out.println("队列中当前元素个数:" + queue.size());
// 遍历队列中的元素
System.out.print("队列中的元素为:");
for (String s : queue) {
System.out.print(s + " ");
}
System.out.println();
}
}
输出结果
队列中当前元素个数:3
获取到的元素为:a
队列中当前元素个数:3
队列中的元素为:b c d
主线程和另一个线程并发地对阻塞队列进行读写操作。在另一个线程中,我们使用了put()
方法向队列中添加元素,这是一个阻塞方法,如果队列已满,它将阻塞当前线程,直到队列中有空闲位置为止。因此,在另一个线程中,当我们向队列中添加第四个元素时,由于队列已满,put()
方法将一直阻塞直到队列有空闲位置。而在主线程中,我们在等待500毫秒后,从队列中取出了一个元素,此时队列中只有三个元素,因此我们成功从队列中获取了一个元素。在输出队列中当前元素个数和遍历队列中的元素时,我们看到只有三个元素,这也验证了我们的推断。
源码解读
数据结构
//数据元素数组
final Object[] items;
//下一个待取出的元素下标索引
int takeIndex;
//下一个待放入的元素下标索引
int putIndex;
//元素个数
int count;
//内部锁
final ReentrantLock lock;
//消费者条件
private final Condition notEmpty;
//生产者条件
private final Condition notFull;
put()
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可中断的锁
lock.lockInterruptibly();
try {
//阻塞队列已满,则将生产者挂起,等待消费者唤醒
while (count == items.length)
notFull.await();
//入队
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
//入队
items[putIndex] = x;
//putIndex进行自增(入队索引)
if (++putIndex == items.length)
putIndex = 0; //环形数组,putIndex指针到数组尽头了,返回头部
count++;
//到这里入队成功队列不为空去唤醒消费者去消费
notEmpty.signal();
}
take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//获取可中断的锁
lock.lockInterruptibly();
try {
//队列为空时挂起消费者,等待生产者唤醒
while (count == 0)
notEmpty.await();
//出队
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//去除要出队的元素
E x = (E) items[takeIndex];
//将刚刚出队的元素所占的坑位置为空
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0; //环形数组 消费到队尾要重置到0从对头重新开始消费
count--;
if (itrs != null)
itrs.elementDequeued();
//此时队列有空位,唤醒生产者继续生产消息
notFull.signal();
return x;
}
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。
LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素,LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。
使用
public class LinkedBlockingQueueDemo {
public static void main(String[] args) {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(3); //创建一个容量为3的阻塞队列
// 启动一个生产者线程
new Thread(new Producer(queue)).start();
// 启动一个消费者线程
new Thread(new Consumer(queue)).start();
}
// 生产者线程
static class Producer implements Runnable {
private final LinkedBlockingQueue<String> queue;
public Producer(LinkedBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
// 不断向队列中添加元素
for (int i = 1; i <= 5; i++) {
String element = "element" + i;
queue.put(element); // put方法会阻塞线程,直到队列中有空闲位置为止
System.out.println("生产者生产了:" + element);
Thread.sleep(1000); // 模拟生产过程中的耗时操作
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费者线程
static class Consumer implements Runnable {
private final LinkedBlockingQueue<String> queue;
public Consumer(LinkedBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
// 不断从队列中取出元素进行消费
while (true) {
String element = queue.take(); // take方法会阻塞线程,直到队列中有可用元素为止
System.out.println("消费者消费了:" + element);
Thread.sleep(2000); // 模拟消费过程中的耗时操作
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
该示例中,创建了一个容量为3的LinkedBlockingQueue,然后启动一个生产者线程和一个消费者线程,生产者线程向队列中添加元素,消费者线程从队列中取出元素进行消费。由于LinkedBlockingQueue是一个阻塞队列,因此在队列已满或为空时,生产者和消费者线程都会被阻塞,直到队列中有空闲位置或有可用元素为止。
源码解读
数据结构
//容量
private final int capacity;
//元素数量
private final AtomicInteger count = new AtomicInteger();
/**
* 链表头节点
* Invariant: head.item == null 初始化时为null
*/
transient Node<E> head;
/**
* 链表尾节点
* Invariant: last.next == null 初始化时为null
*/
private transient Node<E> last;
// take锁
private final ReentrantLock takeLock = new ReentrantLock();
//当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = takeLock.newCondition();
//put锁
private final ReentrantLock putLock = new ReentrantLock();
//当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();
put()
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
//新建节点
Node<E> node = new Node<E>(e);
//获取put锁
final ReentrantLock putLock = this.putLock;
//获取元素数量
final AtomicInteger count = this.count;
//加锁
putLock.lockInterruptibly();
try {
//元素数量和容量相等时,进入阻塞等待消费者唤醒
while (count.get() == capacity) {
notFull.await();
}
//入队
enqueue(node);
//队列长度+1
c = count.getAndIncrement();
//如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可继续入队)
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
// 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
signalNotEmpty();
}
private void enqueue(Node<E> node) {
//直接加到last后面,last指向入队元素
last = last.next = node;
}
take()
public E take() throws InterruptedException {
E x;
int c = -1;
//获取元素数量
final AtomicInteger count = this.count;
//获取take锁
final ReentrantLock takeLock = this.takeLock;
//加锁
takeLock.lockInterruptibly();
try {
//元素数量为0时当前线程阻塞,等待唤醒
while (count.get() == 0) {
notEmpty.await();
}
//出队
x = dequeue();
//长度减一返回原值
c = count.getAndDecrement();
//如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// head节点本身是不存储任何元素的
// 这里把head删除,并把head下一个节点作为新的值
// 并把其值置空,返回原来的值
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
DelayQueue
DelayQueue是Java中的一个并发队列实现,它是PriorityQueue的一个特殊变体,用于存储实现了Delayed接口的元素。Delayed接口定义了一个getDelay()方法,该方法返回元素需要延迟的时间,以及一个compareTo()方法,该方法用于比较元素之间的顺序。
使用
public class DelayQueueDemo {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();
// 创建两个任务,一个延迟5秒,一个延迟10秒
DelayedTask task1 = new DelayedTask("task1", 5, TimeUnit.SECONDS);
DelayedTask task2 = new DelayedTask("task2", 10, TimeUnit.SECONDS);
// 将任务加入DelayQueue
delayQueue.put(task1);
delayQueue.put(task2);
// 循环获取任务并执行
while (!delayQueue.isEmpty()) {
DelayedTask task = delayQueue.take();
System.out.println("execute task: " + task.getName());
}
}
// 实现Delayed接口的任务类
static class DelayedTask implements Delayed {
private final String name;
private final long delayTime;
private final long expireTime;
public DelayedTask(String name, long delay, TimeUnit unit) {
this.name = name;
this.delayTime = TimeUnit.MILLISECONDS.convert(delay, unit);
this.expireTime = System.currentTimeMillis() + this.delayTime;
}
public String getName() {
return name;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((DelayedTask) o).expireTime);
}
}
}
输出结果
execute task: task1
execute task: task2
上面的代码创建了一个DelayQueue,并向其中加入两个任务,一个延迟5秒,一个延迟10秒。然后循环从DelayQueue中获取任务并执行,因为第一个任务要延迟5秒执行,所以先输出执行第一个任务的信息,等5秒后再执行第二个任务。
源码解读
数据结构
//内部锁
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列,存储元素,用于保证延迟低的优先执行
private final PriorityQueue<E> q = new PriorityQueue<E>();
//用于标记当前是否有线程在排队
private Thread leader = null;
//用于表示现在是否有可取的元素条件
private final Condition available = lock.newCondition();
put()
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//入队
q.offer(e);
// 若入队的元素位于队列头部,说明当前元素延迟最小
if (q.peek() == e) {
//将leader置为空
leader = null;
//唤醒阻塞在available上的线程
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
这是调用了优先级队列的的入队方法
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
// 获取当前队列的元素个数
int i = size;
// 如果队列已满,则扩容
if (i >= queue.length)
grow(i + 1);
// 元素个数加1
size = i + 1;
// 如果队列为空,直接将元素放入队列第一个位置
if (i == 0)
queue[0] = e;
else
// 如果队列不为空,将元素插入到合适的位置
siftUp(i, e);
return true;
}
take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
//死循环等待主动跳出
for (;;) {
//取最早过期的元素
E first = q.peek();
//为空就阻塞等待数据到来
if (first == null)
available.await();
else {
//获取到期时间
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();//到期就弹出该元素
first = null; // don't retain ref while waiting
if (leader != null)
//如果有线程争抢的Leader线程,则进行无限期等待
available.await();
else {
//leader为空就把当前线程复制给leader
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//等待剩余等待时间
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
//释放leader
leader = null;
}
}
}
}
} finally {
//leader为空同时有下一个元素则释放凭证唤醒
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}