您的位置:首页 > 编程语言

并发编程实战学习笔记(十一)-原子变量与非阻塞同步机制

2017-04-05 17:44 996 查看

原子变量在非阻塞算法的应用

实现基础

用底层的原子机器指令(例如比例并交换指令)代替锁来确保数据在并发访问中的一致性。

缺点

非阻塞算法在设计与实现上比阻塞算法都要复杂得多。

优点

在可伸缩性和活跃性上拥有巨大的优势。由于非阻塞算法可以使多个线程竞争相同的数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并且极大地减少调度开销。

不存在死锁和其它活跃性问题。

即使原子变量没有用于非阻塞算法的开发,它们也可以用做一种“更好的volatile类型变量”。

原子变量与锁适用的不同并发场景

在中低程序的竞争、锁占用时间不长的情况下,原子变量能提供更高的可伸缩性

基于原子变量而实现的非阻塞算法本质是一个乐观锁,认为在大部分时间不会有竞争或者只有较小的竞争,所以用较短时间的自旋这个小代价来代替因为竞争锁而阻塞,出现的线程调度成本。

而在高强度的竞争下,锁能够更有效的地避免竞争。

高强度的竞争下,非阻塞算法会出现大量在自旋的的线程,由此导致的CPU资源耗用会大于因线程挂起而导致的上下文切换。这个时候,不如直接使用锁机制来得高效。

非阻塞算法举例

非阻塞算法通常比基于锁的算法更为复杂。创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单个变量上,同时还要维护数据的一致性。

非阻塞的栈

public class ConcurrentStack<E> {

AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();

public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = head.get();
newHead.next = oldHead;
} while (!head.compareAndSet(oldHead, newHead));
}

public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = head.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
//如果更新的时候其它线程已经先于本线程更新成功了,则会循环进行此操作,直到它先于其它线程更新成功
//通过这种方式,就实现了将原子修改的范围缩小到单个变量上,同时也能保持数据一致性。
} while (!head.compareAndSet(oldHead,newHead));
return oldHead.item;
}

static class Node<E> {
final E item;
Node<E> next;
public Node(E item) { this.item = item; }
}

}


非阻塞链表

参考Michael-Scott提出的非阻塞链接队列算法。(Michael and Scott ,1996)

这里只简单过一下,核心的要点,上述栈的实现已经有所体现。

双重检查加锁(DCL)

问题分析

当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值(在这种情况下只是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态却是失效的,这意味着可以看到对象处于无效或错误的状态。

获得锁的线程并未将对象完全初使化完成,比如部分变量初使化代码尚未执行,但对应的static引用已经指向了引用的地址,另一个线程此时来访问就会认为引用不是null,从而继续往下走逻辑出现未知的风险。

解决办法

jdk5.0以后,把变量声明为volatile类型,那么就能启用DCL,并且这种方式对性能的影响很小。

虽然也有人用临时变量解决了这个问题,但相对volatile方法来说,要复杂的多,所以建议选择上述方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  并发 编程