1.线程安全队列
两种方式:
- 阻塞算法:出队入队用同一把锁或分别用一把锁实现
- 非阻塞算法:循环CAS的方式实现
ConcurrentLinkedQueue是基于链接节点的无界线程安全队列,添加元素到队尾,从头部获取元素
2.ConcurrentLinkedQueue结构
每个节点由节点元素item和指向下一个节点的引用next构成
默认情况下head节点存储的元素为空,head节点=tail节点
private transient volatile Node<E> tail=head;
3.入队列
3.1 入队列过程
将节点添加到尾部,示意图:
区分tail节点和尾结点:尾结点可能是tail节点,也可能是tail.next节点
插入元素时,tail.next不为空时,则将tail节点移到新节点,加入尾节点后面(tail每次移动两个节点)
入队源代码如下:
public boolean offer(E e) {
if (e == null) throw new NullPointerException()
// 入队前,创建一个入队节点
Node<E> n = new Node<E>(e);
retry:
// 死循环,入队不成功反复入队。
for (;;) {
// 创建一个指向tail节点的引用 (非尾结点)
Node<E> t = tail;
// p用来表示队列的尾节点,默认情况下等于tail节点。
Node<E> p = t;
for (int hops = 0; ; hops++) {
// 获得p节点的下一个节点。
Node<E> next = succ(p);
// next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
if (next != null) {
// 循环了两次及其以上,并且当前节点还是不等于尾节点,HOPS默认值1
if (hops > HOPS && t != tail)
continue retry;
p = next;
}
// 如果p是尾节点,则设置p节点的next节点为入队节点。
else if (p.casNext(null, n)) {
/*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/
if (hops >= HOPS)
// CAS更新tail节点,允许失败
casTail(t, n);
return true;
}
//p有next节点,表示p的next节点是尾节点,则重新设置p节点
else {
//p后移操作
p = succ(p);
}
}
}
}
主要做两件事情:
- 定位尾结点
- 使用cas算法将尾结点的下一个节点(p.next)设置为入队节点,不成功则重试
3.2 定位尾结点
通过tail节点找到尾结点,尾结点可能是tail节点,也可能是tail的尾结点
succ(p)代码如下:
final Node<E> succ(Node<E> p) {
//获取下一个节点
Node<E> next = p.getNext();
//p == next只有初始化时才会出现,p后移
return (p == next) head : next;
}
3.3 设置入队节点为尾结点
p.casNext(null,n)
将入队节点设置为当前队列尾结点的next节点(现在的p节点)
- p为null,表示是尾节点,更新该节点值
- p不为null,表示有其他线程更新,重新获取尾结点
3.4 HOPS的设计意图
tail节点和尾结点的距离大于HOPS(默认1)时才会更新tail节点,因为tail是volatile变量,写操作的开销比读操作的开销大,本质上是通过增加volatile读的次数减少volatile写的次数,达到性能提升的效果
4.出队列
示意图:
只有当head节点没有元素并且出队时,才会更新head节点
也是通过HOPS来减少CAS对head节点的更新,提高出队效率
代码如下:
public E poll() {
Node<E> h = head;
// p表示头节点,需要出队的节点Node<E> p = h;
for (int hops = 0;; hops++) {
// 获取p节点的元素
E item = p.getItem();
// 如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,
// 如果成功则返回p节点的元素。
if (item != null && p.casItem(item, null)) {
if (hops >= HOPS) {
// 将p节点下一个节点设置成head节点
Node<E> q = p.getNext();
updateHead(h, (q != null) q : p);
}
return item;
}
// 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外
// 一个线程修改了。那么获取p节点的下一个节点
Node<E> next = succ(p);
// 如果p的下一个节点也为空,说明这个队列已经空了
if (next == null) {
// 更新头节点。
updateHead(h, p);
break;
}
// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
p = next;
}
return null;
}