何为阻塞队列?
阻塞队列,重点在于阻塞二字,意思就是支持阻塞插入和阻塞移除的队列。
阻塞插入:当队列中的元素满了,插入操作线程将阻塞直至队列有空闲空间;
阻塞移除:当队列中的元素为空,就是指没有元素时,移除操作线程将阻塞直至队列不为空;
从队细容量的角度划分,队列可分为有界和无界两种,从这个角度来说,对于无界队列,插入的阻塞操作是不会发生了。
阻塞队列的经典使用场景?
1、生产-消费者模式任务:这个是最常见的吧,生产者就往阻塞队列中"插入"元素,消费者就"移除"元素;
2、延时任务,这个也常见吧,比如某些下单后30分钟未支付自动取消、用户支付后自动下发券码短信等等
jdk提供哪些阻塞队列?
1.ArrayBlockingQueue
基于数组实现的有界阻塞队列,先进先出(first-in-first-out,简称FIFO)的方式排序,默认情况下不保证线程公平性的访问队列,意思就是说,线程A先阻塞,并不意味着线程A就可以先访问到元素,都是随机的竞争访问。看看下ArrayBlockingQueue的默认构造器就一目了然了
public ArrayBlockingQueue(int capacity) { this(capacity, false); } //第二个参数就是代表是否需要公平锁
当然,我们可以使用这个构造器来实现公平的阻塞队列,这个时候就严格的FIFO了
ArrayBlockingQueue abq = new ArrayBlockingQueue(500,true);
其公平性保证来自于此构造方法使用了可重入锁ReentrantLock
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//可重入锁
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
但是显然使用了可重入锁保证公平性后会降低应用的QPS,所以到底是要公平性还是要高性能,自己权衡。其方法主要包括
1.public boolean add(E e) {……} //向队列中插入指定元素,若对列容量满了则抛出IllegalStateException异常,success返回true,
e为空的话抛出空指针异常,也就是说,add方法插入失败的话就只能通过抛出异常的方式;
2.public boolean offer(E e) {……} //向队列插入指定元素,若对列容量满了则返回false,success返回true,e为空的话抛出空指针异常,此方法优于add方法;
3.public void put(E e) throws InterruptedException {……} //向队列中插入元素,若对列已满则阻塞进入等待;
4.public E poll() {……} //从队列中移除(取出)元素,若对列中没有元素则直接返回null;
5.public E take() throws InterruptedException {……} //从队列中移除(取出)元素,若对列中没有元素则一直阻塞;
其中方法 2~5在jdk提供的阻塞队列类中基本都有体现,接下的类中就不赘述了。
2.LinkedBlockingQueue
基于链表结构实现的有界阻塞队列,先进先出(first-in-first-out,简称FIFO)的方式排序,默认大小为 MAX_VALUE=2的31次方,不保证公平性访问元素
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
同理也可以使用下面的构造器来实现访问的公平性
public LinkedBlockingQueue(Collection extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
略……
}
3.PriorityBlockingQueue
支持自定义排序的无界阻塞队列,默认按自然升序排序元素
public PriorityBlockingQueue() { this(DEFAULT_INITIAL_CAPACITY, null); }
在初始化时也支持自定义比较器
public PriorityBlockingQueue(int initialCapacity,Comparator super E> comparator) {
if (initialCapacity < 1) throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
4.DelayQueue
无界阻塞队列,支持延时获取获取元素,创建元素时可以指定延时时间,只有延期满才能从中获取元素。DelayQueue中的元素必须实现Delayed接口。这个比较常用,比如上面说到的,用户支付后30s自动发送券码短信功能(我只截取了部分代码,但是里面的延时任务逻辑还是很清晰的……)
public class SendMessageDelay implements Delayed {
private String code;
private String phone;
private long timeout;
public SendMessageDelay(String code, String phone, long timeout) {
this.code = code;
this.phone = phone;
this.timeout = timeout + System.nanoTime();
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o == this) return 0;
SendMessageDelay t = (SendMessageDelay) o;
long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
//发送逻辑,省略了部分代码
public void send() {
try {
SendSmsUtil.send(phone, "自定义短信文案"+code);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
public class SendMessageDelayServiceImpl implements SendMessageDelayService {
private static Logger log = LoggerFactory.getLogger(SendMessageDelayServiceImpl.class);
@Autowired
private SendFailService sendFailService;
@Override
public boolean sendDelay(OrderInfo orderInfo) {
boolean flag = true;
DelayQueue queue = new DelayQueue();
//下单30秒后执行发短息逻辑
queue.put(new SendMessageDelay(orderInfo.getOrderId(), orderInfo.getPhone(), TimeUnit.NANOSECONDS.convert(30, TimeUnit.SECONDS)));
try {
//take表示获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件返回结果,在我这里take方法就返回DendMessageDelay实例对象
queue.take().send();
log.info("orderid={},send message successful~", orderInfo.getOrderId());
return flag;
} catch (InterruptedException e) {
log.error("orderid=" + orderInfo.getOrderId() + ",send ms exceptioin={}", e);
//持久化 异常订单数据,有重试机制
sendFailService.addFailRecord(orderInfo);
flag = false;
}
return flag;
}
}
5.LinkedBlockingDeque
由链表结构组成的双向阻塞队列,D就表示double,支持对队列的两端进行插入和移除元素,所以相对于单向阻塞队列,并发操作时也更快一些。可通过如下方法实例化:
//指定最大容量为固定值:2的31次方
public LinkedBlockingDeque() { this(Integer.MAX_VALUE); }
//指定容量初始化
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
6.LinkedTransferQueue
基于链表结构的无界阻塞队列,比上面的几个队列多了transfer和tryTransfer方法
/*若存在消费者正在等待元素,transfer就将生产者中的元素立即传递给消费者,若没有则等待直到有消费者消费才返回*/
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
/*若存在消费者在等待元素,立即将生产者的元素传递给消费者,若没有也立即返回false*/
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
return true;
if (!Thread.interrupted())
return false;
throw new InterruptedException();
}
7.SynchronousQueue
不存储元素的阻塞队列 ,每个阻塞插入操作都必须等待一个阻塞移除操作,否则不能继续插入元素(所谓的不存储元素就是这个意思,一放进去就得移除),从这点来看SynchronousQueue的qps高于普通的阻塞队列,默认情况下非公平性访问队列。
阻塞队列的实现原理
阻塞队列的实现从代码层面看就是使用了Condition(多条件模式),里面封装了根据条件阻塞线程的操作
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair); //实例化可重入锁
notEmpty = lock.newCondition(); //Condition配合Lock使用,notEmpty表示阻塞插入条件
notFull = lock.newCondition(); //notFull表示阻塞移除条件
}
接着看在阻塞插入和阻塞移除的await方法,需要阻塞时比较关键的一步就是LockSupport.park(this),它实现的阻塞
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
long savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
再接着看LockSupport.park(this)方法
/*禁止当前线程调度*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
/*保存阻塞的线程*/
setBlocker(t, blocker);
/*阻塞线程*/
unsafe.park(false, 0L);
setBlocker(t, null);
}
public native void park(boolean var1, long var2);
park是个native修饰的方法,用来阻塞当前线程,它依赖于具体的操作系统而实现,HotSpot中实现的park方法可以参考这里https://blog.csdn.net/hengyunabc/article/details/28126139
当下列情况发生时park方法会返回:
1.当前线程执行了unpark方法(多次和一次效果是一致的);
2.当前线程被中断;
3.park方法中的相对时间到期;
4.发生了未知异常;