今天来说一说ConcurrentLinkedQueue类,ConcurrentLinkedQueue类是线程安全的非阻塞无界的FIFIO队列,通过CAS算法进行入队和出队。
ConcurrentLinkedQueue类中主要的成员变量如下:
private transient volatile Node head; //内部双向列表的头节点,volatile保证内存可见性。
private transient volatile Node tail; //内部双向列表的尾节点,volatile保证内存可见性。
private static final sun.misc.Unsafe UNSAFE;//UNSAFE实例用来执行CAS算法。
下面对ConcurrentLinkedQueue类中常用方法源码进行介绍。
offer(E e)
在队列尾部添加一个指定元素,由于队列是无界的,所以该方法一直会返回true。由于使用CAS算法,所有线程不会被阻塞挂起。
public boolean offer(E e) {
//(1)检查参数是否为null,是则抛出空指针异常
checkNotNull(e);
final Node newNode =new Node(e);
for (Node t =tail, p = t;;) {
Node q = p.next;
//(2)如果q为null,说明p是尾部节点,则进行插入操作
if (q ==null) {
// (3)使用CAS算法设置p的next节点
if (p.casNext(null, newNode)) {
//(4)CAS成功,则说明新增节点已经插入到链表,然后设置当前尾节点。
if (p != t)
casTail(t, newNode); // (5)尾结点的设置允许失败,不影响后序的操作。
return true;
}
}
else if (p == q)
//(6)多线程操作时,由于poll操作移除元素后可能会把head变为自引用,也就是head的next还是head,这个时候需要重新找新的head
p = (t != (t =tail)) ? t :head;
else
// (7)寻找尾结点
p = (p != t && t != (t =tail)) ? t : q;
}
}
由于CAS算法同一时间只能有一个线程修改成功,其他失败的线程会通过一次次循环尝试进行CAS操作,直到CAS成功才返回。相比于阻塞算法挂起调用线程,CAS算法以CPU资源换取上下文切换带来的开销。
add(E e)
public boolean add(E e) {
return offer(e);
}
add方法内部还是调用的offer方法,这里就不进行赘述了。
poll()
在队列头部获取并移除一个元素,如果队列为空则返回null。
public E poll() {
restartFromHead:
for (;;) {
for (Node
h = head, p = h, q;;) { E item = p.item;
//(1)当前节点不为null,则将当前节点通过cas算法设置为null,cas操作成功则表示当前节点从链表中移除
if (item != null && p.casItem(item, null)) {
if (p != h)
//(2)通过cas算法更新head节点。
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//(3)当前队列为空,返回null。
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//(4)如果当前节点被自引用,则重新寻找新的队列头节点
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
poll方法在移除一个元素是,只是简单地使用CAS操作当前节点的item值为null,然后通过重新设置头节点将该元素从队列里面移除,被移除的节点就成为了孤立的节点,这个节点会在垃圾回收时被回收。如果在执行分支中发现头节点被修改了,要跳到外层循环重新获取新的头节点。
peek()
获取头部一个元素,并不移除,如果队列为空则返回null。
public E peek() {
restartFromHead:
for (;;) {
for (Node
h = head, p = h, q;;) { E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
peek方法和poll方法差不多,只是前者没有调用casItem方法,将获取到的节点的item设置为null。
size()
获取队列中的元素,是一个不精准的统计。
public int size() {
int count = 0;
//(1)first()方法获取第一个非哨兵节点,没有则返回null
// succ()方法获取当前节点的next节点,如果是自引用节点则返回真正的头节点
for (Node
p = first(); p != null; p = succ(p)) if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}
由于ConcurrentLinkedQueue中都是使用的CAS没有加锁,而size方法统计的时候有可能其他的线程对ConcurrentLinkedQueue队列进行操作,从而导致统计元素的不准确性。
remove()
如果队列中存在指定元素则删除该元素,如果存在多个则删除第一个并返回true,否则返回false。
public boolean remove(Object o) {
if (o != null) {
Node
next, pred = null; //(1)从head节点向tail节点进行遍历
for (Node
p = first(); p != null; pred = p, p = next) { boolean removed = false;
E item = p.item;
//(2)队列中的节点和指定的节点相等则使用CAS将队列中的节点设置为null,由于CAS只能一个线程操作成功,失败的线程会继续遍历查找队列中是否还有其他匹配的节点。
if (item != null) {
if (!o.equals(item)) {
next = succ(p);
continue;
}
removed = p.casItem(item, null);
}
next = succ(p);
if (pred != null && next != null) // unlink
pred.casNext(p, next);
if (removed)
return true;
}
}
return false;
}
containes(Object o)
判断队列中是否有与指定元素相等的元素,有则返回true,否则返回false。和size方法一样是一个不精度的匹配。
public boolean contains(Object o) {
if (o == null) return false;
for (Node
p = first(); p != null; p = succ(p)) { E item = p.item;
if (item != null && o.equals(item))
return true;
}
return false;
}
通过源码可以看出来ConcurrentLinkedQueue中都是使用的CAS没有加锁,而contains方法遍历队列的时候有可能其他的线程对ConcurrentLinkedQueue队列进行操作,从而导致统计元素的不准确性。
今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。