public boolean offer(E e) {
if (e == null) throw new NullPointerException();
Node
for (;;) { //1
Node
Node
if (t == tail) { //4
if (s == null) { //5
if (t.casNext(s, n)) { //6
casTail(t, n); //7
return true; //8
}
} else {
casTail(t, s); //9
}
}
}
}
在有锁得情况下我们只要让获得锁得线程更新,其它线程等待即可解决并发更新的问题,但是在上述的单向链表结构中有更好的无锁解决方法。
1.代码1 啊! 死循环,对,就是利用反复轮询的重复一段逻辑操作。
2.代码2 代码3 先用两个临时变量指向CLQ的尾和尾的下一个节点。这样有什么好处?直接用tail和tail.getNext不行吗?我们说了。这是一个无锁得方法。可能有多个线程同时执行到代码3处,因为临时变量是每线程的而tail是公共的。这样成功执行到代码3的线程都有自己当时的临时CLQ队列结构引用。为后面的判断做好准备。
3.开始判断 代码4 证明 在代码2 和代码4之间没有被其它线程修改过,因为有可能已经被修改了。那么这时进入新的轮询。
4.代码5 在看代码5之前我们先要明确一个概念就是把一个Node放入一个CLQ队列有两步操作。第一步是tail的next指向新的节点。第二步是tail指向新的节点。代码5 先判断是不是有线程已经在完成加入一个节点的第一步,如果是就帮助它完成第二步,再次进入循环。如果没有线程已经完成第一步。那就自己来完成插入节点的第一步,当然就是调用casNext比较更新的原子操作。上文已经讲过。再来完成插入元素的第二步,以上逻辑由代码6、代码7完成。注意看代码8 恒为真? 为什么?自己调用casTail如果成功返回真毫无疑问。如果失败为什么也返回真?答案很简单,这是因为如果失败说明一定有其它线程进入了代码9 帮自己完成了插入一个节点的第二步操作。所以自己操作肯定是失败的。所以也返回真。
从上面的代码分析可以看出打造一个无锁得并发容器处处都要十分小心。这也是CLQ的高明之处。
我们再来看看删除一个元素的代码:
Java代码
public E poll() {
for (;;) {
Node
Node
Node
if (h == head) { //4
if (h == t) { //5
if (first == null) //6
return null; //7
else
casTail(t, first); //8
} else if (casHead(h, first)) { //9
E item = first.getItem(); //10
if (item != null) { //11
first.setItem(null); //12
return item; //13
}
// else skip over deleted item, continue loop,
}
}
}
}
1.同样是轮询,当h!=head的时候继续循环。因为在代码1和代码4之间已经有其它线程删除了头元素。从而造成h != head.
2.代码5 是否是空的CLQ。
3.如果是空的CLQ判断头得下一节点是否是null.因为只有时空的才说明没有元素。否则有可能其它线程正在插入元素造成first!=null,这时就帮助其它线程完成为指针更新操作。再继续轮询。
4.如果是非空的CLQ用casHead来原子更新头节点。因为删除一个CLQ的元素是从头开始删除的。如果失败说明有其它线程在删除元素。继续轮询。
5.代码10 如果第一个元素的内容为空说明有线程已经执行到代码12了。所以又开始轮询。
6.只有成功执行到代码13才正真是由当前线程完成了删除一个元素操作。CLQ的peek()操作和poll操作只差代码12的操作,即一个删除元素,一个不删除元素。
在CLQ中迭代器的方法也很精妙:
Java代码
private E advance() {
lastRet = nextNode;
E x = nextItem;
Node
for (;;) {
if (p == null) {
nextNode = null;
nextItem = null;
return x;