BlockingQueues在java.util.concurrent包下,提供了线程安全的队列访问方式,当阻塞队列插入数据时,如果队列已经满了,线程则会阻塞等待队列中元素被取出后在插入,当从阻塞队列中取数据时,如果队列是空的,则线程会阻塞等待队列中有新元素。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。
认识BlockingQueue:
阻塞队列是一个具有阻塞添加和阻塞删除功能的队列,数据结构如下
阻塞添加
所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。阻塞删除
阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)数据结构
上图可以多线程环境下,通过队列很容易实现数据共享, 比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了,让我们一起来见识下它的常用方法
BlockingQueue的使用:
阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:
负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到这个队列的上限值。队列达到上限值之后生产线程将会被阻塞,直到消费的线程对这个队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,当队列为空时,消费线程将会被阻塞,除非队列中有新的对象被插入。
BlockingQueue的核心方法:
-方法\行为 | 抛异常 | 特定的值 | 阻塞 | 超时 |
---|---|---|---|---|
插入方法 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
移除方法 | poll(),remove(o) | take() | poll(timeout, timeunit) | |
检查方法 | element() | peek() |
行为解释:
1.抛异常:如果操作不能马上进行,则抛出异常
2. 特定的值:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
3. 阻塞:如果操作不能马上进行,操作会被阻塞
4. 超时:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false
插入方法:
- add(E e) : 添加成功返回true,失败抛IllegalStateException异常
- offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
- put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法:
- remove(Object o) :移除指定元素,成功返回true,失败返回false
- poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
- take():获取并移除此队列头元素,若没有元素则一直阻塞。
检查方法
- element() :获取但不移除此队列的头元素,没有元素则抛异常
- peek() :获取但不移除此队列的头;若队列为空,则返回 null。
BlockingQueue的数据结构:
实现类ArrayBlockingQueue的基本使用:
ArrayBlockingQueue:是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。另外它以FIFO先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部
BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take();
有点需要注意的是ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。创建公平与非公平阻塞队列代码如下:
//默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
//公平阻塞队列
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(2,true);
//构造方法源码
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
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();
}
实现类LinkedBlockingQueue的基本使用:
LinkedBlockingQueue:是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,如果需要的话,这一链式结构可以自定义一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。建议指定队列大小,默认大小在添加速度大于删除速度情况下可能造成内存溢出,LinkedBlockingQueue队列也是按 FIFO(先进先出)排序元素
构造方法源码:
//默认大小为Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
//创建指定大小为capacity的阻塞队列
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}
//创建大小默认值为Integer.MAX_VALUE的阻塞队列并添加c中的元素到阻塞队列
public LinkedBlockingQueue(Collection extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
ArrayBlockingQueue和LinkedBlockingQueue的区别:
1.队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
2.数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
3.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
4.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。DelayQueue中的元素必须实现 java.util.concurrent.Delayed
接口,这个接口的定义非常简单:
public interface Delayed extends Comparable {
long getDelay(TimeUnit unit);
}
getDelay()
方法的返回值就是队列元素被释放前的保持时间,如果返回0
或者一个负值
,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()
方法释放此对象。
从上面Delayed 接口定义可以看到,它还继承了Comparable
接口,这是因为DelayedQueue中的元素需要进行排序,一般情况,我们都是按元素过期时间的优先级进行排序
public class DelayedElement implements Delayed {
private long expired;
private long delay;
private String name;
DelayedElement(String elementName, long delay) {
this. name = elementName;
this. delay= delay;
expired = ( delay + System. currentTimeMillis());
}
@Override
public int compareTo(Delayed o) {
DelayedElement cached=(DelayedElement) o;
return cached.getExpired()> expired?1:-1;
}
@Override
public long getDelay(TimeUnit unit) {
return ( expired - System. currentTimeMillis());
}
@Override
public String toString() {
return "DelayedElement [delay=" + delay + ", name=" + name + "]";
}
public long getExpired() {
return expired;
}
}
设置这个元素的过期时间为3s
public class DelayQueueExample {
public static void main(String[] args) throws InterruptedException {
DelayQueue queue= new DelayQueue<>();
DelayedElement ele= new DelayedElement( "cache 3 seconds",3000);
queue.put( ele);
System. out.println( queue.take());
}
PriorityBlockingQueue队列
PriorityBlockingQueue是一个没有边界的队列,所以不会阻塞生产者,它的排序规则和 java.util.PriorityQueue
一样。需要注意,PriorityBlockingQueue中不允许插入null对象。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
SynchronousQueue队列
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
拓展:
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件
在最近的RocketMQ的项目中用到阻塞队列,代码如下:实时监听MQ消息,收到消息,先做简单 的处理(存入队列),在开启线程消费消息(监听和消费消息解耦合)
@Component("MQCP_CID_PH_SSP_DEFAULT")
public class MqcpCosumerServiceImpl implements IMqcpCosumerService,InitializingBean,DisposableBean {
private Logger LOGGER =LoggerFactory.getLogger(getClass());
private static MQCPConsumer pushConsumer = null;
private static LinkedBlockingQueue msgQueue;
private static final int QUEUE_MAX_SIZE = 20000;
@Autowired
private ISystemConfigService systemConfigService;
@Autowired
private IMqcpMessageService mqcpMessageService;
public static void setPushConsumer(MQCPConsumer pushConsumer) {
MqcpCosumerServiceImpl.pushConsumer = pushConsumer;
}
public static void setMsgQueue(LinkedBlockingQueue msgQueue) {
MqcpCosumerServiceImpl.msgQueue = msgQueue;
}
public void initMsgQueue(){
setMsgQueue(new LinkedBlockingQueue(QUEUE_MAX_SIZE));
}
public MqcpCosumerServiceImpl() {
initMsgQueue();
}
@Override
public void initMQCPCosumer(){
try {
Properties p = SystemResourceUtil
.getPropertisByName(ResourceFileNameConstants.PROPERTIES_MQCP_CLIENT);
p.setProperty(MQCPConstant.INSTANCE_NAME, systemConfigService.getSysGUID());
setPushConsumer(MQCPFactory.createConsumer(p));
MQCPMessageFilter mqcpFilter = new MQCPMessageFilter();
List list = new ArrayList();
//根据tag 过滤消息,MQCP只取出发送消息时设置了该tag值的消息
list.add("T_APS_APPL_INFO");
list.add("T_APS_LOAN_AGREEMENT");
list.add("T_APS_EXPENSE_APPL");
list.add("T_APS_LOAN_LOG");
list.add("T_ICORE_CGI_POLICY_INFO");
mqcpFilter.setTags(list);
// 实时监听消息
pushConsumer.subscribe(SystemResourceUtil.getPropertiesValueByKey(
ResourceFileNameConstants.PROPERTIES_MQCP_CLIENT, "TOPIC_ID_ILOAN"), mqcpFilter, new MQCPMessageListener() {
@Override
public MQCPConsumeStatus pushMessage(List messageList) {
try {
for (MQCPMessage msg : messageList) {
LOGGER.info(String.format("存证MQCPCosumer监听到Topic_id:[%s]的消息key:[%s],准备存入队列",msg.getTopic(),msg.getKey()));
msgQueue.put(msg);
}
return MQCPConsumeStatus.CONSUME_OK;
} catch (Exception e) {
LOGGER.error("存证MQCPCosumer消息存入队列异常", e);
return MQCPConsumeStatus.CONSUME_FAIL;
}
}
});
pushConsumer.start();
LOGGER.info("初始化存证MQCPConsumer订阅服务:success");
} catch (MQCPException e) {
LOGGER.error("初始化存证MQCPCosumer订阅服务异常", e);
}
}
@Override
public void shutDownMQCPCosumer() {
try {
if(pushConsumer !=null){
pushConsumer.shutdown();
}
} catch (Exception e) {
LOGGER.error("存证MQCPCosumer关闭异常", e);
}
}
@Override
public void afterPropertiesSet() throws Exception {
initMQCPCosumer();//开启消费者
new Thread(new InsertTable()).start();
}
@Override
public void destroy() throws Exception {
shutDownMQCPCosumer();
}
class InsertTable implements Runnable{
@Override
public void run() {
while(!Thread.interrupted()){
try {
MQCPMessage msg = msgQueue.take();
LOGGER.info(String.format("存证MQCP消息入库线程开启[%s],消息TAG:[%s]", Thread.currentThread().getName(),msg.getTag()));
mqcpMessageService.insertTable(msg);
} catch (Exception e) {
LOGGER.error("存证MQCP消息入库异常",e);
}
}
LOGGER.warn(String.format("存证线程已停止[%s]", Thread.currentThread().getName()));
}
}
}