阻塞队列
概述
分离向队列放入(生产者)、从队列拿出(消费者)两个角色。它们需要由不同的线程来担当,这就需要考虑线程安全问题。
- 用锁保证线程安全;
- 用条件变量让等待非空线程与等待不满线程进入等待状态,让 CPU 空转停止。
接口代码
/**
目前队列存在的问题
<ol>
<li>很多场景要求<b>分离</b>生产者、消费者两个<b>角色</b>、它们得由不同的线程来担当,而之前的实现根本没有考虑线程安全问题</li>
<li>队列为空,那么在之前的实现里会返回 null,如果就是硬要拿到一个元素呢?只能不断循环尝试</li>
<li>队列为满,那么再之前的实现里会返回 false,如果就是硬要塞入一个元素呢?只能不断循环尝试</li>
</ol>
解决方法
<ol>
<li>用锁保证线程安全</li>
<li>用条件变量让 poll 或 offer 线程进入<b>等待</b>状态,而不是不断循环尝试,让 CPU 空转</li>
</ol>
*/
public interface BlockingQueue<E> { // 阻塞队列
void offer(E e) throws InterruptedException;
boolean offer(E e, long timeout) throws InterruptedException;
E poll() throws InterruptedException;
}
单锁实现
Java 中要防止代码段交错执行,需要使用锁,有两种选择:
- synchronized 代码块,属于关键字级别提供锁保护,功能少;
- ReentrantLock 类,功能丰富。
以 ReentrantLock 为例
代码
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 单锁实现
* @param <E> 元素类型
*/
@SuppressWarnings("all")
public class BlockingQueue1<E> implements BlockingQueue<E> {
private final E[] array;
private int head;
private int tail;
private int size;
public BlockingQueue1(int capacity) {
array = (E[]) new Object[capacity];
}
private ReentrantLock lock = new ReentrantLock();
private Condition headWaits = lock.newCondition();
private Condition tailWaits = lock.newCondition();
private boolean isEmpty() {
return size == 0;
}
private boolean isFull() {
return size == array.length;
}
@Override
public void offer(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
while (isFull()) {
tailWaits.await(); // 等多久
}
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
size++;
headWaits.signal();
} finally {
lock.unlock();
}
}
@Override
public boolean offer(E e, long timeout) throws InterruptedException { // 毫秒 5s 带超时的版本,可以只等待一段时间,而不是永久等下去
lock.lockInterruptibly();
try {
long t = TimeUnit.MILLISECONDS.toNanos(timeout);
while (isFull()) {
if (t <= 0) {
return false;
}
t = tailWaits.awaitNanos(t); // 最多等待多少纳秒 1s 4s 返回值代表剩余时间
}
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
size++;
headWaits.signal();
return true;
} finally {
lock.unlock();
}
}
@Override
public E poll() throws InterruptedException {
lock.lockInterruptibly();
try {
while (isEmpty()) {
headWaits.await();
}
E e = array[head];
array[head] = null; // help GC
if (++head == array.length) {
head = 0;
}
size--;
tailWaits.signal();
return e;
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return Arrays.toString(array);
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue1<String> queue = new BlockingQueue1<>(3);
queue.offer("任务1");
new Thread(()->{
try {
queue.offer("任务2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "offer").start();
new Thread(()->{
try {
System.out.println(queue.poll());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "poll").start();
}
}
双锁实现
单锁的缺点在于:
- 生产和消费几乎是不冲突的,唯一冲突的是生产者和消费者它们有可能同时修改 size
- 冲突的主要是生产者与消费者之间:多个 offer 线程修改 tail,多个 poll 线程修改 head
利用双锁,提高性能。(使生产消费同时进行)
- 一把锁保护 tail
- 另一把锁保护 head
代码
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 双锁实现
* @param <E> 元素类型
*/
@SuppressWarnings("all")
public class BlockingQueue2<E> implements BlockingQueue<E> {
private final E[] array;
private int head;
private int tail;
private AtomicInteger size = new AtomicInteger();
private ReentrantLock tailLock = new ReentrantLock();
private Condition tailWaits = tailLock.newCondition();
private ReentrantLock headLock = new ReentrantLock();
private Condition headWaits = headLock.newCondition();
public BlockingQueue2(int capacity) {
this.array = (E[]) new Object[capacity];
}
private boolean isEmpty() {
return size.get() == 0;
}
private boolean isFull() {
return size.get() == array.length;
}
@Override
public String toString() {
return Arrays.toString(array);
}
@Override
public void offer(E e) throws InterruptedException {
int c; // 添加前元素个数
tailLock.lockInterruptibly();
try {
// 1. 队列满则等待
while (isFull()) {
tailWaits.await(); // offer2
}
// 2. 不满则入队
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
// 3. 修改 size
/*
size = 6
*/
c = size.getAndIncrement();
if (c + 1 < array.length) {
tailWaits.signal();
}
/*
1. 读取成员变量size的值 5
2. 自增 6
3. 结果写回成员变量size 6
*/
} finally {
tailLock.unlock();
}
// 4. 如果从0变为非空,由offer这边唤醒等待非空的poll线程
// 0->1 1->2 2->3
//目的是减少唤醒poll时的加锁解锁次数
// b. 从0->不空, 由此offer线程唤醒等待的poll线程
if(c == 0) {
headlock. Lock(); // offer_1 offer_2 offer_3
try {
headWaits.signal();
} finally {
headLock.unlock();
}
}
}
@Override
public E poll() throws InterruptedException {
E e;
int c; // 取走前的元素个数
headLock.lockInterruptibly();
try {
// 1. 队列空则等待
while (isEmpty()) {
headWaits.await(); // poll_4
}
// 2. 非空则出队
e = array[head];
array[head] = null; // help GC
if (++head == array.length) {
head = 0;
}
// 3. 修改 size
c = size.getAndDecrement();
// 3->2 2->1 1->0
// poll_1 poll_2 poll_3
if (c > 1) {
headWaits.signal();
}
/*
1. 读取成员变量size的值 5
2. 自减 4
3. 结果写回成员变量size 4
*/
} finally {
headlock. Unlock();
}
// 4. 队列从满->不满时 由poll唤醒等待不满的 offer 线程
//a. 从满->不满, 由此poll线程唤醒等待的offer线程
if(c == array.length) {
tailLock.lock();
try {
tailWaits.signal(); // ctrl+alt+t
} finally {
tailLock.unlock();
}
}
return e;
}
@Override
public boolean offer(E e, long timeout) throws InterruptedException {
return false;
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue2<String> queue = new BlockingQueue2<>(3);
queue.offer("元素1");
queue.offer("元素2");
new Thread(()->{
try {
queue.offer("元素3");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "offer").start();
new Thread(()->{
try {
queue.poll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "poll").start();
}
}
补充
注意
- JDK 中 BlockingQueue 接口的方法命名与我的示例有些差异;
- 方法 offer(E e) 是非阻塞的实现,阻塞实现方法为 put(E e);
- 方法 poll() 是非阻塞的实现,阻塞实现方法为 take()。
锁:保证多行代码执行下的原子性。
拒绝锁的嵌套,可以避免死锁。
- 在IntelliJ IDEA中直接通过内置终端执行命令:
- 找到Java进程ID(PID):
jps
(列出所有Java进程) - 查看堆栈信息以查找可能的死锁:
jstack <PID>
(将替换为你的Java进程ID) - 分析输出结果中的“Found one Java-level deadlock”部分以及线程堆栈跟踪,找出可能导致死锁的部分。
虚假唤醒:从 tailWaits 中唤醒的线程,会与新来的线程争抢锁,从而有可能导致条件变化。(唤醒后应该重新检查条件,看是不是需要重新进入等待)
Java代码使用了java.util.concurrent.atomic.AtomicInteger
类来实现对整数值的原子操作。AtomicInteger
是线程安全的,能够在多线程环境下保证其内部值的修改(如自增、自减)具有原子性,不会出现数据竞争的问题。
来源
路漫漫其修远兮,吾将上下而求索。