作用
支持并发操作的线程安全队列,正如其名linKedQueue, 说明这个队列是用链表实现的队列, 对于队列我们直到主要的操作就是入队列和出队列,入队和出队操作同一个链表必要会有线程安全问题,如何避免:入队和出队方法直接加synchronized锁, 可以, 但是效率太低了, 入队和出队 在链表头尾,直接有头尾指针指向,这也说明入对出队时间是极短,并不需要进行耗时业务操作,如果直接加synchronized 发生竞争就会变成重量级锁,说不定入队耗时,还没有加锁释放锁进行内核态切换耗时多,所以面对这种需要线程安全,但是耗时又极短,最好的方法就是使用cas和循环补偿。 所以ConcurrentLinkedQueue队列的所有线程安全为了避免锁带来的消耗,全是使用的cas和自旋。
呃, 虽然cas和自旋 在这种情况下效率高于synchronized,但是问题就是编程很复杂。。。。。
思考优化点
呃, 既然是链表,为了入队和出队去循环,所以ConcurrentLinkedQueue 里面有头尾指针,这也就能直接得到头和尾, 入队就是把当前节点链接到尾节点,出队就是把当前节点链表到后继。那么思考一个问题, head 和 tail 指针 需要每次入队出队都要移动吗, 也就是 说实现方式要用 cas 抢夺 tail 然后 pre.next = tail。
作为一个并发链表队列, 如果1000个线程都进行入队操作,那么tail就要进行1000次有移动, 所以效率上有一些影响,同意的出队也有同样的问题。
需要有一个哨兵节点, 这也就不需要考虑head和tail之间在队列中有没有节点插入和删除的问题。各自做各自的。
数据结构
链表
private static class Node<E> {
volatile E item; //值
volatile Node<E> next; //指向下一个节点
}
Node中的个个方法
呃 我们直到 node就是链表中的元素, 所以入队和出队等操作,处理要处理(看情况 即有没有达到处理阈值)head和tail 之外,还需要处理node的next指向, 包括 入队的时候前一个节点的next指向入队节点, 以及入队节点item的赋值。 以及出队item的赋值为null, next指向自己(从链表中断开)。
private static class Node<E> {
volatile E item; //值
volatile Node<E> next; //指向下一个节点
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item); //普通变量的写,节点初始化,没有入队,不会共享,所以不需要volatile的写保障可见。普通变量的写效率高于volatile的写
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);//cas设置item的值 比如:出队的时候, 多个线程对同一个节点进行出队,都需要给该节点的item设为null, 只能有一个成功。
}
void lazySetNext(Node<E> val) { //出队的时候,需要把next指向自己, 但是由于头指向以及指向出队节点的下一个节点,所以当前节点的next的不需要很强的可见性
UNSAFE.putOrderedObject(this, nextOffset, val);//putOrderedObject 取决于编译器要不要优化给可见性。
}
boolean casNext(Node<E> cmp, Node<E> val) { //修改next
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE; //魔法类
private static final long itemOffset; //item 在Node中的地址偏移量
private static final long nextOffset; //next在Node中的地址偏移量
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
构造函数
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null); //节点的初始化
}
队列中, 存在一个哨兵节点
offer 入队 (链表是无界,所以次方法永远不会返回false)
public boolean offer(E e) { //队列 入队
checkNotNull(e); //入队的值 不能为null
final Node<E> newNode = new Node<E>(e); //创建节点 这里的final保证了 newNode拿到的时完整的对象, 即final变量 禁止编译重排的影响
for (Node<E> t = tail, p = t;;) { //无锁 入队,cas+循环补偿 思考:这里入队为啥不是cas tail指向newNode, 成功之后在把之前的tail.next = newNode
Node<E> q = p.next;
if (q == null) { //p是队列末尾
// p is last node
if (p.casNext(null, newNode)) { //cas入队, 失败的需要继续循环
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time t 和 p 之间需要至少2个节点的阈值
casTail(t, newNode); // Failure is OK. cas 设置尾节点
return true;
}
// Lost CAS race to another thread; re-read next cas失败,那么说明当前节点的next指针已经指向别的node了, 当前线程重新循环获取最新的next
} else if (p == q) //这种情况比较特殊,要和入队进行比较 能出现这种情况说明 p节点是以被别的线程出队了
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else //p 不为null, 说明别的线程以及offer成功了
// Check for tail updates after two hops. 得p 在第二个节点才会去判断头又没有改变
p = (p != t && t != (t = tail)) ? t : q; //如果别的线程offer成功了,并且修改了tail, p指向最新的tail 这样就可以快速到达尾巴节点提升效率, 否则 p 指向自己的后继q
}
}
因为是无锁队列,所以入队和出队, 入队和入队, 出队和出队都是同时进行的, 这里为啥会出翔 p==q 等 继续看poll()方法,因为和出队有关系
poll
/*出队从对头*/
public E poll() {
restartFromHead: //标签
for (;;) { //cas 补偿机制
for (Node<E> h = head, p = h, q;;) {
E item = p.item; //记录原先值
if (item != null && p.casItem(item, null)) { //如果item不为空并且cas修改成功
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time 同样和入队方法一样, 移动头指针存在至少2个节点的阈值
updateHead(h, ((q = p.next) != null) ? q : p);//如果后继不为null那么head执行后继,否者head指向当前节点
return item;
}
else if ((q = p.next) == null) { //说明到了尾节点, p的所有前驱节点item都被别的线程设置为了null
updateHead(h, p); //帮助别的线程移动头指针
return null;
}
else if (p == q)//说明p节点已经被别的线程poll了
continue restartFromHead; //重新从head开始遍历
else
p = q;//指向自己的后继
}
}
}
updateHead
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p)) //cas需改head指向
h.lazySetNext(h); //next指向自己,断开, 因为head已经指向自己的后继,所以如果有后面来的线程,他们读head,所以next不需要强可见性。
}
解释 为啥入队出队 p == q
updateHead的时候会把原先节点的next指向自己, 如果这个lazySetNext不是立即刷内存,但是如果cpu自己刷了,那别的线程就可以看到。
peek 查看对头item
//查看对头节点的值
public E peek() {
restartFromHead: //标签
for (;;) {
for (Node<E> h = head, p = h, q;;) { //指向head
E item = p.item; //volatile的读
if (item != null || (q = p.next) == null) {//item不为空 或则和 p的后继为null
updateHead(h, p); //更新head
return item;
}
else if (p == q) //p节点已经出队
continue restartFromHead; //重新从head 找对头
else //继续遍历后继
p = q;
}
}
}
first 获取对头node
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;
}
}
}
peek和first 一个是返对头item , 一个是返回对头Node, 为啥要写两个方法,peek为啥不写 first().item??, peek和first两个方法在和poll方法都是同时可以进行的,因为是无锁, 这样就会导致 first的出来的Node节点会存在刚刚好被poll的, 所以item为null, 而peek方法返回null,除非是队列到了队列末尾整所有节点都是item为null, 所以peek 不能直接复用 first.item , 如果非要使用还需要判断first.item如果为null, 继续进行for循环
remove 从队列中删除一个元素
public boolean remove(Object o) {
if (o != null) {
Node<E> next, pred = null;
for (Node<E> p = first(); p != null; pred = p, p = next) {//p指向对头节点 pred为p的前驱
boolean removed = false; //是否移除标志
E item = p.item;
if (item != null) { //没有被被别的线程poll或者remove
if (!o.equals(item)) { //是否和目标移除对象相等
next = succ(p); //获取后继 注意:如果p被poll了他的后继就是自己, 所以这种情况下p的后继为head
continue;
}
removed = p.casItem(item, null); //进行cas移除 是否能够竞争过别的线程
}
next = succ(p); //获取后继
if (pred != null && next != null) // unlink 断开连接
pred.casNext(p, next);
if (removed) //移除了
return true;
}
}
return false;
}
size 获取队列元素个数
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p)) //从栈顶进行遍历,取后继
if (p.item != null) //item不为空
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE) //没有找过最大值
break;
return count;
}
此方法直接遍历判断 item不为null, 直接数量+1, 如果遍历到后面前面的节点被别的线程poll了, 所以数量是不准确的, 所以在这种无锁并发队列中 获取队列元素个数的方法不是准确的,所以用处不怎么大,这个size方法, 而且size遍历整个队列,效率也不高
isEmpty判断队列是否包含元素
前面提到的size方法如果用来判断队列是否包含元素是不妥的,因为size方法计数过程中是不准确的。
public boolean isEmpty() {
return first() == null; //获取栈顶元素
}
迭代器
呃,迭代器的实现不不是强一致性,因为整个队列是并发无锁的,所以在迭代的时候别的线程进行poll remove offer等操作, 所以迭代器的实现就是每次hashNext的时候都是取获取最新的队列头节点。