CAS
CAS,compare and swap的缩写,意思为比较并交换。
从JDK 5 开始,Doug lea 给我们提供了Coucurrent包,让我们解决并发问题,CAS就是实现这个包的基础。
CAS包含三个操作数--内存位置,预期位置和新值,当且仅当内存位置的值与预期位置的值相同的时候,处理器才会把该位置的值修改为新值,否则,处理器不做任何操作。无论如何,处理器都会在CAS指令之前返回该位置的值。
CAS指令允许算法执行读-修改-写操作,不用担心其他线程同时修改变量,因为该变量只能同时只有一个线程可以修改,其他线程失败后可以重新读取在操作。
利用CAS指令,可以完成java的非组阻塞算法,synchronized是阻塞算法,整个并发包都是在CAS基础之上完成的,相比synchronized,J.U.C在性能上有了很大的提高,可以很高效的完成原子操作。
ConcurrentLinkedQueue
ConcurrentLinkedQueue是基于CAS实现的非阻塞的线程安全的队列,它遵循先入先出的原则。当添加一个元素的时候,它被添加到队列的尾部,当获取一个元素的时候,它会返回队列头部的元素。
ConcurrentLinkedQueue是由head和tail节点组成,每个节点都有item和指向下一个节点的指针,组成了链表结构的队列,默认的构造函数是head和tail为null。
private static class Node<E> {
volatile E item;
volatile Node<E> next;
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
下面看一下它的主要常用方法:
add方法
添加元素的过程主要做了两件事,第一是把要添加的节点作为当前队列尾节点的next节点,第二是把更新尾节点,如果尾节点的next节点不为空,就把新添的节点作为尾节点,如果尾节点的next为空,就把新添节点作为尾节点的next节点。
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
}
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
1. 找到当前队列的尾节点,利用CAS把新增节点作为尾节点的下一个节点,保证最后一个节点是新添加的节点,其他失败的线程会更新p指向这个新节点.
2. 如果tail节点与p相距2个节点,修改tail指向.
3. 如果p的next节点指向自己的话,说明节点p已经被删除.
假设多个线程同时都要添加元素,第一次添加元素的时候,队列为空,全部线程都能执行到q=p.next=null,此时,只有一个线程才能利用CAS把p的next节点设为newNode,这时判断p==t,所以并不会利用CAS把newNode更新为tail节点,casNext方法执行成功,直接返回true.其它失败的线程走到最后一个else分支,把p指向newNode.这时,p的next肯定为空,第二个竞争成功执行的线程利用CAS把自己的节点放到newNode的next,由于t指向一个空节点,p指向newNode节点,所有p!=t,利用CAS把新添加的节点设为tail节点,依次类推,每隔两个节点更新一次tail节点,这样通过增加对volatile的读操作减少对volatile的写操作,写操作的开销要高于读操作,所以入队效率总体是提升的.
看一下p==q的情况,这是由于当前节点被删除导致的.next指向自己,这种节点没有任何价值,当遇到这种节点的时候,一般是返回头结点,从头节点开始遍历,找到tail节点.如果发生在执行过程中,tail被其他线程修改的情况,直接把tail节点作为尾节点,避免重新查找的开销.
p = (p != t && t != (t = tail)) ? t : q
这句代码中 != 并不是原子操作,程序执行的时候,先取得t的值,再执行t=tail,然后把tail赋值的t与原来的t进行比较值是否相等. 例如:
public static void main(String[] args) {
int a = 1;
int b = 2;
System.out.println(a != (a = b));
}
在多线程中,t != t才能成立,取得左边的t,右边的tail可能被其他线程修改,所以新的节点作为tail节点,把p指向最新的tail节点.
当队列为空的时候
当添加第一个元素之后,
当添加第二个元素之后,
头结点永远是一个空节点.,每当添加两个节点之后就要更新tail节点,依次这样循环添加.
poll方法
poll和add方法相似,并不是每次都更新头结点,当队列为空时,直接返回空,每经过读取两个节点更新头结点.
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
注意,head节点是一个空节点.
当队列为空时,代码执行到q=p.next=null这一条件判断,如果这时候线程没有添加元素的话,进入updateHead方法进行判定,
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
此时h和p都是null,所以h != p 为false,直接返回null;如果这时在执行q=p.next=null之前,线程在队列中添加了一个元素,代码会进入p=q,把p指向p的next节点,此时p不为空,利用CAS把item设为null,这时p != h,根据条件把p作为当前链表的头节点,然后h节点的next指向自己,也就变成了哨兵节点,其它cas失败的线程进入(q=p.next)==null,直接返回null。
什么时候p=q那??
初始状态:
poll出e后:
利用CAS把e置为null,p != h,所以更新头结点,把p.next设为头结点,这时候原来的head指向自己.
poll出f后:
head节点不为空,直接把head的item设为空,p=head,直接返回item.
poll出g后:
tail滞后于head,如果这时候添加一个元素h,在offer方法中代码走到p=q中,根据条件判断直接p指向head节点.
队列判空
public int size() {
int count = 0;
for (Node<E> 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;
}
在判断队列为空的时候,并不建议使用size方法,主要是为了队列在高并发情况下的数据访问的正确性,由于遍历的时候有可能其他线程会对队列状态进行修改,所有数据有可能有错误,如果队列中节点较多的情况下,遍历所有的节点,性能会有较大影响,可以考虑isEmpty方法.
public boolean isEmpty() {
return first() == null;
}
总结
ConcurrentLinkedQueue是基于链表线程安全的非阻塞队列,采用先入先出的顺序对链表排序,当添加一个元素的时候直接加到队尾,当获取一个元素的时候,直接返回头部元素。通过CAS操作,在线程安全的前提下,提高了队列操作效率。很是佩服Doug lea 把这个类设计的如此精妙,ConcurrentLinkedQueue是研究CAS最好的类,通过无锁操作来实现了高并发.Doug lea的代码写的非常简洁,但是并不意味着你能理解.但是一旦掌握了它的核心思想,对于AQS的理解有很大帮助,所以知识的积累还是不断学习总结的过程.