文章目录
并发 Queue
非阻塞队列-ConcurrentLinkedQueue
简介
ConcurrentLinkedQueue
是一个基于链接节点的无界线程安全队列,使用Compare And Swap
(CAS)算法实现- 无锁实现,效率比阻塞队列更好
- Tomcat 中
NioEndPoint
中的每个poller
里面就维护一个ConcurrentLinkedQueue<Runnable>
用来作为缓冲存放任务
源码分析
- 属性: volatile 类型的
Node
节点
head
:存放链表第一个item为null的节点tail
:则并不是总指向最后一个节点Node
节点内部则维护一个变量item
用来存放节点的值,next
用来存放下一个节点,从而链接为一个单向无界列表
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
- 方法
add
:内部调用offer
offer
:添加数据,添加到尾部(tail)
public boolean offer(E e) {
checkNotNull(e); e为null则抛出空指针异常
// 构造Node节点构造函数内部调用unsafe.putObject
final Node<E> newNode = new Node<E>(e);
// 从尾节点插入
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// 如果q=null说明p是尾节点则插入
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// cas成功说明新增节点已经被放入链表,然后设置当前尾节点(包含head,1,3,5.。。个节点为尾节点)
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// 多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
// 重新找新的head,因为新的head后面的节点才是激活的节点
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
poll
:取数据,从头部获取数据(head)并且移除
public E poll() {
restartFromHead:
// 死循环
for (;;) {
// 死循环
for (Node<E> h = head, p = h, q;;) {
// 保存当前节点值
E item = p.item;
// 当前节点有值则cas变为null(1)
if (item != null && p.casItem(item, null)) {
//cas成功标志当前节点以及从链表中移除
if (p != h) // 类似tail间隔2设置一次头节点(2)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 当前队列为空则返回null(3)
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//自引用了,则重新找新的队列头节点(4)
else if (p == q)
continue restartFromHead;
else//(5)
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
-
peek
:取数据,从头部获取数据(head),不移除。代码与poll
类似,只是少了castItem()
-
size
:获取当前队列元素个数,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确 -
isEmpty
:都是调用first()
,略有不同
Node<E> first() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
remove
:如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回true,否者返回 false
简单示例
// 使用 `Compare And Swap`(CAS) 算法实现无锁队列,效率更好
ConcurrentLinkedQueue<String> noBlockQueue = new ConcurrentLinkedQueue<>();
/** 添加方法 */
// add 内部调用 offer 方法
noBlockQueue.add("a");
noBlockQueue.offer("b");
//[a, b]
System.out.println(noBlockQueue);
/** 获取数据的方法 */
// 获取数据,并且从队列移除
System.out.println(noBlockQueue.poll());
System.out.println(noBlockQueue);
// 获取,但是不会从队列移除
System.out.println(noBlockQueue.peek());
System.out.println(noBlockQueue);
// isEmpty 和 size 内部都是调用 first()方法。
// 因为使用CAS没有加锁,所以是一个不精确的数据
System.out.println(noBlockQueue.isEmpty());
System.out.println(noBlockQueue.size());
输出:
[a, b]
a
[b]
b
[b]
false
1
阻塞队列-BlockingQueue
阻塞队列的主要功能其实并不是在于提高并发时的性能,更多的是简化多线程之间的数据共享,如生产者-消费者模式
常用队列
ArrayBlockingQueue
:有限队列,底层数组。内部维护定长数组用于缓存对象,维护两个整型常量,分别标识队列头部和尾部在数组中的位置。默认非公平锁LinkedBlockingQueue
:可以设置无限,底层链表。维护一个数据缓冲队列(链表构成)PriorityBlockingQueue
:带有优先级的阻塞队列
常用方法
-
boolean add(E e)
:增加一个元索,如果队列已满,则抛出一个异常IllegalStateException("Queue full")
-
boolean offer(E e)
:添加一个元素并返回true,如果队列已满,则返回false。不阻塞 -
boolean offer(E e, long timeout, TimeUnit unit)
:队列满时指定阻塞时间,指定时间内还不能加入返回false -
void put(E e)
:添加一个元素,如果队列满,则阻塞,直到有空间 -
E take()
:移除并返回队列头部的元素,如果队列为空,则阻塞 -
E poll(long timeout, TimeUnit unit)
:取出队首的对象。如果在指定时间内,有数据可取这返回;超时后依然没有数据则返回null -
boolean remove(Object o)
:移除数据 -
int drainTo(Collection<? super E> c)
:一次性从队列获取所有可用的数据对象。可以提升获取数据的效率
LinkedBlockingQueue 与 ArrayBlockingQueue 区别
- 底层实现不同
ArrayBlockingQueue
底层是数组LinkedBlockingQueue
底层是链表(Node对象)
- 队列容量不同
ArrayBlockingQueue
是有限队列,构造的时候一定要指定初始化大小。当然如果手动指定其capacity
为 `Integer.MAX_VALUE`` 也可以理解为无限LinkedBlockingQueue
默认是无限(Integer.MAX_VALUE),也可以手动缩小其容量,指定capacity
。- 在使用无限队列的过程中,注意当生产大大的快于消费时的 OOM 情况
- 同步实现原理不同(锁实现不一样)
ArrayBlockingQueue
是一把锁。即其添加与移除操作使用的是同一个ReenterLock
对象,只是有两个同步Condition
进行同步。LinkedBlockingQueue
是锁分离,提高并发吞吐量。添加与移除操作是不同的锁,由于链表的缘故,添加和移除操作分别作用于队列的前端和尾端,使用两把不同的锁分离了添加和移除操作。即take与take之间一把锁,put与put之间一把锁
LinkedBlockingQueue
锁分离源码
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
ArrayBlockingQueue
一把锁源码
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
....
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
简单示例
- 添加数据
LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(2);
// 1- 增加一个元索,如果队列已满,则抛出一个 IllegalStateException: Queue full 异常
blockingQueue.add("a");
System.out.println(blockingQueue);
// blockingQueue.add("a2");
// IllegalStateException: Queue full
// blockingQueue.add("a3");
// 2-添加一个元素并返回true, 如果队列已满,则返回false,不会有异常
boolean b = blockingQueue.offer("b");
System.out.println(b);
boolean b2 = blockingQueue.offer("b2");
System.out.println(b2);
System.out.println(blockingQueue);
// 3-队列满时指定阻塞时间,在规定时间内如果无法添加则返回false,不会有异常
boolean c = blockingQueue.offer("c", 2, TimeUnit.SECONDS);
System.out.println(c);
boolean c1 = blockingQueue.offer("c1", 2, TimeUnit.SECONDS);
System.out.println(c1);
System.out.println(blockingQueue);
// 清除数据
blockingQueue.clear();
// 4- 添加一个元素,如果队列满,则阻塞
blockingQueue.put("d");
System.out.println(blockingQueue);
输出:
[a]
true
false
[a, b]
false
false
[a, b]
[d]
- 取出数据
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);
blockingQueue.add("a");
blockingQueue.add("b");
// 1- 移除并返问队列头部的元素, 如果队列为空,则返回null,不会有异常
String poll = blockingQueue.poll();
System.out.println(poll);
System.out.println(blockingQueue);
// 2- 有数据直接返回;队列空时指定阻塞时间,在规定时间内还是没有数据,则返回 null
String poll1 = blockingQueue.poll(2, TimeUnit.SECONDS);
System.out.println(poll1);
String poll2 = blockingQueue.poll(2, TimeUnit.SECONDS);
System.out.println(poll2);
System.out.println(blockingQueue);
blockingQueue.add("a");
blockingQueue.add("b");
// 4-返回但不移除队列头部的元素, 如果队列为空,则返回 null,不会有异常
String peek = blockingQueue.peek();
System.out.println(peek);
System.out.println(blockingQueue);
// blockingQueue.clear();
// 6-移除并返回队列头部的元素,如果队列为空,则阻塞
blockingQueue.take();
System.out.println(blockingQueue);
blockingQueue.clear();
blockingQueue.add("a");
blockingQueue.add("b");
// 7- 批量获取
ArrayList<String> list = new ArrayList<>();
int i = blockingQueue.drainTo(list);
System.out.println(i);
System.out.println(list);
输出:
a
[b]
b
null
[]
a
[a, b]
[b]
2
[a, b]
Deque(Double-Ended-Queue)双向队列(JDK1.6 以后)
简介
- Deque双向队列,允许在队列的头部或者尾部执行出队和入队操作
- 与 Queue 相比,具有更加复杂的功能
LinkedList
、ArrayDeque
、LinkedBlockingDeque
是实现了Deque
接口的常见双向队列。
- 由于
ArrayDeque
基于数组实现,拥有高效的随机访问性能,但是由于其扩展时需要重新分配内存并进行数组复制,写性能不如LinkedList
LinkedBlockingDeque
是线程安全的双向队列
LinkedBlockingDeque
- 使用链表结构,每一个队列节点都维护一个前驱节点和后驱节点
- 没有进行读写锁的分离,效率较低