本文共 1744 字,大约阅读时间需要 5 分钟。
用底层的原子机器指令(例如比例并交换指令)代替锁来确保数据在并发访问中的一致性。
非阻塞算法在设计与实现上比阻塞算法都要复杂得多。
基于原子变量而实现的非阻塞算法本质是一个乐观锁,认为在大部分时间不会有竞争或者只有较小的竞争,所以用较短时间的自旋这个小代价来代替因为竞争锁而阻塞,出现的线程调度成本。
高强度的竞争下,非阻塞算法会出现大量在自旋的的线程,由此导致的CPU资源耗用会大于因线程挂起而导致的上下文切换。这个时候,不如直接使用锁机制来得高效。
非阻塞算法通常比基于锁的算法更为复杂。创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单个变量上,同时还要维护数据的一致性。
public class ConcurrentStack{ AtomicReference > head = new AtomicReference >(); public void push(E item) { Node newHead = new Node (item); Node oldHead; do { oldHead = head.get(); newHead.next = oldHead; } while (!head.compareAndSet(oldHead, newHead)); } public E pop() { Node oldHead; Node newHead; do { oldHead = head.get(); if (oldHead == null) return null; newHead = oldHead.next; //如果更新的时候其它线程已经先于本线程更新成功了,则会循环进行此操作,直到它先于其它线程更新成功 //通过这种方式,就实现了将原子修改的范围缩小到单个变量上,同时也能保持数据一致性。 } while (!head.compareAndSet(oldHead,newHead)); return oldHead.item; } static class Node { final E item; Node next; public Node(E item) { this.item = item; } }}
参考Michael-Scott提出的非阻塞链接队列算法。(Michael and Scott ,1996)
这里只简单过一下,核心的要点,上述栈的实现已经有所体现。当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值(在这种情况下只是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态却是失效的,这意味着可以看到对象处于无效或错误的状态。
获得锁的线程并未将对象完全初使化完成,比如部分变量初使化代码尚未执行,但对应的static引用已经指向了引用的地址,另一个线程此时来访问就会认为引用不是null,从而继续往下走逻辑出现未知的风险。
jdk5.0以后,把变量声明为volatile类型,那么就能启用DCL,并且这种方式对性能的影响很小。
虽然也有人用临时变量解决了这个问题,但相对volatile方法来说,要复杂的多,所以建议选择上述方法。转载地址:http://rtlxb.baihongyu.com/