队列
在java中队列是一个先进先出的数据结构,属于集合Collection的一个子类,如下图
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列非阻塞,一个是以BlockingQueue接口为代表的阻塞队列,具体如下:
- ArrayDeque, (非阻塞数组双端队列) 线程不安全,不支持null (https://www.cnblogs.com/lxyit/p/9080590.html)
- PriorityQueue, (非阻塞优先级队列) 线程不安全(https://www.cnblogs.com/demingblog/p/6485193.html)
- ConcurrentLinkedQueue, (非阻塞基于链表的并发队列) 线程安全(http://ifeve.com/concurrentlinkedqueue/)
- DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口)
- ArrayBlockingQueue, (基于数组的并发阻塞队列)
- LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
- LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
- PriorityBlockingQueue, (带优先级的无界阻塞队列)
- SynchronousQueue (并发同步阻塞队列)
阻塞队列与非阻塞队
阻塞队列与普通队列的区别在于,
- 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素,非阻塞直接返回null;
- 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,非阻塞直接返回false或抛异常;
ConcurrentLinkedDeque
ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常其性能好于BlockingQueue。
- 它是一个基于链接节点(Node)的无界线程安全队列,以CAS自旋锁方式保证线程安全。
- 该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。
源码如下:
//head 和 tail 全为volatile修饰的Node,一线程对一个volatile变量的读,总是
//能看到(任意线程)对这个volatile变量最后的写入”
transient volatile Node<E> head;
private transient volatile Node<E> tail;
// 构造器,Node结构的链表
public ConcurrentLinkedQueue() {
head = tail = new Node<E>();
}
// 维持一个链表且传入初始值
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
for (E e : c) {
// 加入非空元素
Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
if (h == null)
h = t = newNode;
else
t.appendRelaxed(t = newNode);
}
// 全为null时,
if (h == null)
h = t = new Node<E>();
head = h;
tail = t;
}
其他源码可参考:https://blog.youkuaiyun.com/qq_22798455/article/details/81637397
ConcurrentLinkedQueue重要方法:
- add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
- poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
ConcurrentLinkedDeque q = new ConcurrentLinkedDeque();
q.offer("阿里");
q.offer("码云");
q.offer("腾讯");
//从头获取元素,删除该元素
System.out.println(q.poll());
//从头获取元素,不刪除该元素
System.out.println(q.peek());
//获取总长度
System.out.println(q.size());
BlockingQueue
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
- 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
- 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
- 当队列满了的时候进行入队列操作,存储元素的线程会等待队列可用
- 当队列空了的时候进行出队列操作,获取元素的线程会等待队列变为非空
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。阻塞队列是线程安全的。
API
ArrayBlockingQueue
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
源码实例:
/**
*capacity指定队列的大小
*fair指定是否公平
*/
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
// 这两个notEmpty和notFull参数实际上是Condition,而Condition可以把它看做一个阻塞信号
// Condition的子类ConditionObject(是AbstractQueuedSynchronizer的内部类)拥有两个方法signal和
// signalAll方法,前一个方法是唤醒队列中得第一个线程,而signalAll是唤醒队列中得所有等待线程,
// 但是只有一个等待的线程会被选择,这两个方法可以看做notify和notifyAll的变体。
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:
<String> arrays = new ArrayBlockingQueue<String>(3);
arrays.add("李四");
arrays.add("张军");
arrays.add("张军");
// 添加阻塞队列
arrays.offer("张三", 1, TimeUnit.SECONDS);
总结:
- 线程安全的,并且迭代器也是线程安全的,使用Lock加Condition控制并发;
- 在创建队列时需要指定容量,则不能再改动;
- 只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行;
- 队列锁是不公平策略,即唤醒线程的顺序是随机的;
LinkedBlockingQueue
LinkedBlockingQueue是链表式阻塞队列,队列大小的配置是可选的,可以在初始化时指定一个大小,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。
和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
源码如下:
/** 拿锁,同时一个线程可以取数据 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 无数据时拿线程等待 */
private final Condition notEmpty = takeLock.newCondition();
/** 放锁,同时一个线程存数据 */
private final ReentrantLock putLock = new ReentrantLock();
/** 数据满时存线程等待 */
private final Condition notFull = putLock.newCondition();
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingQueue入队方法:add、offer、put方法。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) // 如果容量满了,返回false
return false;
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 放锁加锁,保证调用offer方法的时候只有1个线程
try {
if (count.get() < capacity) { // 再次判断容量是否已满,因为可能拿锁在进行消费数据,没满的话继续执行
enqueue(node); // 节点添加到链表尾部
c = count.getAndIncrement();
if (c + 1 < capacity) // 如果容量还没满
notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
}
} finally {
putLock.unlock();
}
if (c == 0) // 只有等于0时对拿锁进行激活,大于0是不需要,因为大于0时拿锁不会wait
signalNotEmpty(); //可以进行消费
return c >= 0; // 添加成功返回true,否则返回false
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); // 放锁加锁,保证调用put方法的时候只有1个线程
try {
while (count.get() == capacity) {
notFull.await(); // 阻塞并挂起当前线程
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
} finally {
putLock.unlock(); // 释放放锁,让其他线程可以调用put方法
}
if (c == 0) // 只有等于0时对拿锁进行激活,大于0是不需要,因为大于0时拿锁不会wait
signalNotEmpty(); //
}
下面是一个初始化和使LinkedBlockingQueue的例子:
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.add("张三");
linkedBlockingQueue.add("李四");
linkedBlockingQueue.add("李四");
System.out.println(linkedBlockingQueue.size());
总结:
- 线程安全;
- 存和取有独立的锁,可以同时存数据和取数据,但每种任务同时刻只能有一个;
PriorityBlockingQueue
PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。
所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。
另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。
总结:
- 无存储数量上限,内部使用数组保存数据,数据会自动扩容,<64扩大2倍,>=64则扩大1.5倍;
- 线程安全,存取使用一个锁;
- 内部数据可以排序;
SynchronousQueue
SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
总结:
- 内部不存储数据,put后直接将生产的数据直接传递给消费者;
- 存取同一个锁,一次put后只能等到消费后再次put;
- 效率高于LinkBlockingQueue
生产者消费者实例
class ProducerThread implements Runnable {
private BlockingQueue<String> blockingQueue;
private AtomicInteger count = new AtomicInteger();
private volatile boolean FLAG = true;
public ProducerThread(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "生产者开始启动....");
while (FLAG) {
String data = count.incrementAndGet() + "";
try {
boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (offer) {
System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "成功..");
} else {
System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "失败..");
}
Thread.sleep(1000);
} catch (Exception e) {
}
}
System.out.println(Thread.currentThread().getName() + ",生产者线程停止...");
}
public void stop() {
this.FLAG = false;
}
}
class ConsumerThread implements Runnable {
private volatile boolean FLAG = true;
private BlockingQueue<String> blockingQueue;
public ConsumerThread(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "消费者开始启动....");
while (FLAG) {
try {
String data = blockingQueue.poll(2, TimeUnit.SECONDS);
if (data == null || data == "") {
FLAG = false;
System.out.println("消费者超过2秒时间未获取到消息.");
return;
}
System.out.println("消费者获取到队列信息成功,data:" + data);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
public class Test0008 {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
ProducerThread producerThread = new ProducerThread(blockingQueue);
ConsumerThread consumerThread = new ConsumerThread(blockingQueue);
Thread t1 = new Thread(producerThread);
Thread t2 = new Thread(consumerThread);
t1.start();
t2.start();
//10秒后 停止线程..
try {
Thread.sleep(10*1000);
producerThread.stop();
} catch (Exception e) {
// TODO: handle exception
}
}
}
参考:
https://blog.youkuaiyun.com/fuyuwei2015/article/details/72716753
https://blog.youkuaiyun.com/qq_22798455/article/details/82719096