《JAVA并发编程实践》中提供了3中非阻塞算法的示例。
第一个示例,非阻塞计数器。
CAS,比较并交换即Compare-And-Swap。假设CAS有3个操作数--内存位置V、旧的预测值A和新值B,那么它的典型模式为:首先从V中读取值A,由A生成新值B,然后使用CAS原子化地把V的值改成B,并且期间不能有其他线程改变V的值,因为CAS能够发现来自其他线程的干扰。
假设有两个线程,同时执行到 @1 ,获得了value的旧值 ;然后同时执行到 @2 , 根据原则“ 当多个线程试图使用CAS同时更新相同的变量时,其中一个会胜出,并更新变量的值,而其他线程都会失败,重新尝试。 ”可知,其中一个线程完成了加1操作,而另一个线程失败,重新do循环。让我们细细体会一下这个原则是怎么得到的: 一个线程完成了加1操作后,另一个线程使用CAS时,旧的预期值没有变但内存位置V的值已经更新了,所以此时V的值不等于旧的预期值而导致失败!
第二个示例,非阻塞栈
注: AtomicReference<V>
compareAndSet ( V expect, V update)
若当前值与期望值expect相等时,原子化地将update值赋给当前值。
假设有两个线程,同时执行到 @1 ,获得了 栈顶元素,并创建了一个新节点指向当前栈顶;然后同时执行到 @2 , 根据原则“ 当多个线程试图使用CAS同时更新相同的变量时,其中一个会胜出,并更新变量的值,而其他线程都会失败,重新尝试。 ”可知,其中一个线程完成了插入操作,而另一个线程失败,重新do循环。让我们细细体会一下这个原则是怎么得到的:一个线程完成了插入操作后,另一个线程使用CAS时,旧的预期值没有变但当前栈顶的值已经更新了,所以此时栈顶的值不等于旧的预期值而导致失败!
第 三 个示例,非阻塞 链表
首先看 ? 处 ,为什么要判断 curTail == tail.get() 呢?
必须要有这个判断来保证数据结构总能处于一致状态。如果没有这个判断的话可能出现下面状况。
细细思索一下,如果没有 curTail == tail.get() 这个的话,一个线程将元素3加入队列, 而另一个线程却把next指针的指向了3',元素3就这样被丢弃了,这当然是不行的!也就是我们下文所说的第一个诀窍。
插入新的元素涉及到两个指针的更新(两个指针分别为队尾指针和队尾元素的next指针),需要两个操作过程。第一,更新当前队尾元素的next指针,将新元素插入到列表队尾;第二,释放队尾指针,指向新的最末元素。在这两个操作之间,队列处于中间状态,看图示。
图 a 插入前稳定状态
图 b 插入期间,队列处于中间状态
图 c 插入完成后,队列再一次回到稳定状态
有几个诀窍来完成我们的链表。第一个诀窍是即使在多步更新中,也要确保数据结构总能处于一致状态。也就是说,如果线程B到达时发现线程A正在更新中,B能够知晓操作已经部分完成并且知道不能立即开始自己的更新。那么B就开始等待(通过反复检查队列状态)直到A完成更新,这样两个线程就不会相互影响了。第二个诀窍是,确保如果B到达时发现数据结构正在被A修改,在数据结构中应该有足够多的信息让B去替代A完成更新。如果B“帮助”A完成其操作,那么B可以进行自己的操作,而不用等待A的操作的完成。当A恢复后试图完成其操作,会发现B已经替它完成了。
同时实现两个诀窍的方法是:假设队列处于稳定状态,则尾节点的next域指向null,如果队列处于中间状态,tail.next为非空。所以任何线程都能够通过检查tail.next及时地了解队列状态。进一步而言,如果队列处于中间状态,它能够通过推进队尾指针向前移动一个节点把状态恢复为稳定状态,同时结束任何线程正在插入元素的的操作。
假设有两个线程同时执行到A处,此时tailNext为空,所以两个线程同时执行到C, 根据原则“ 当多个线程试图使用CAS同时更新相同的变量时,其中一个会胜出,并更新变量的值,而其他线程都会失败,重新尝试。 ”可知,其中一个线程X完成了插入操作,而另一个线程Y失败,重新while循环。当线程Y再次执行到A处时,tailNext != null,从而执行 tail.compareAndSet(curTail, tailNext) ,它将替代A完成‘释放队尾指针,指向新的最末元素’的操作,从而达到稳定状态来完成自己的插入操作。当然,‘释放队尾指针,指向新的最末元素’这个操作更可能是线程A执行的,一般只有在A执行受挫时,才由B替代执行。