第15章 原子变量与非阻塞同步机制

近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较并交换指令)代替锁老确保数据在并发访问中的一致性。

15.1 锁的劣势

这个不多说了,详细见p262

15.2 硬件对并发的支持

独占锁是一项悲观的技术,它假设最坏的情况。对于细粒度的操作,还有一种乐观的方法,通过这种方法可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试(听起来很像数据库中的乐观锁啊)。

在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment)以及交换(Swap)等指令,这些指令足以实现各种互斥体,而这些互斥体又可以实现一些更复杂的并发对象。

15.2.1 比较并交换

在大多数处理器架构中采用的方法是实现一个比较并交换(CAS)指令。

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会挂起(这与获取锁的情况不同:当获取锁失败时,线程将被挂起),而是被告知在这次竞争中失败,并可以再次尝试。

15.2.2 非阻塞的计数器

程序15-2中的CaseCounter使用了CAS实现了一个线程安全的计数器。

CaseCounter不会阻塞,但如果其他线程同时更新计数器,那么会多次执行重试操作。

15.2.3 JVM对CAS的支持

在java5.0之前,如果不编写明确的代码,那么就无法执行CAS。在java5.0中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS操作(好像就是那些原子变量),并且JVM把它们编译为底层硬件提供的最有效方法。在支持CAS的平台上,运行时把他们编译为相应的机器指令。在最坏情况下,如果不支持CAS指令,那么JVM将使用自旋锁。

15.3 原子变量类

15.3.1 原子变量是一种“更好的volatile”

从这节的描述来看,原子变量利用的是硬件对并发的支持,即CAS

15.3.2 性能比较:锁与原子变量

构造一个测试基准,其中将比较为随机数字生成器(PRNG)的几种不同的实现。在PRNG中,在生成下一个随机数字时需要用到上一个数字,所以在PRNG中必须记录前一个数值并将其作为状态的一部分。程序15-4和15-5给出了线程安全的PRNG的两种实现,一种使用ReentrantLock,另一种使用AtomicInteger。

15.4 非阻塞算法

如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被成为无锁(Lock-Free)算法。如果在算法中仅将CAS用于协调线程之间的操作,并且能正确实现,那么这它既是一种无阻塞算法,又是一种无锁算法。

15.4.1 非阻塞的栈

非阻塞算法通常比基于锁的算法更为复杂。创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单个变量上,同时还要维护数据的一致性。程序清单15-6的ConcurrentStack中给出了如何通过原子引用来构建栈的示例。

15.4.2 非阻塞的链表

链接队列需要单独维护头指针和尾指针。有两个指针指向位于尾部的节点:当前最后一个元素的next指针,以及尾节点。当成功插入一个元素时,这两个指针都要采用原子操作来更新。初看起来,这个操作无法通过原子变量来实现。在更新这两个指针时需要不同的CAS操作,并且如果第一个CAS成功,但第二个CAS失败,那么队列将处于不一致的状态。而且即使这两个CAS都成功了,那么在执行这两个CAS之间,仍可能有另一个线程会访问这个队列。

程序15-7的LinkedQueue中给出了Michael-Scott提出的非阻塞链接队列算法中的插入部分,在ConcurrentLinkedQueue中使用的正是该算法。在许多队列算法中,空队列通常都包含一个“哨兵”节点或者“哑”节点,并且头节点和尾节点在初始化都指向该哨兵节点。尾节点通常要么指向哨兵节点(如果队列为空),即队列的最后一个元素,要么(当有操作正在执行更新时)指向倒数第二个元素。

这里的重点是理解那个“中间状态”以及“稳定状态”。(上面的图印刷错了,是队列不是对立)。上面操作中的“中间状态”如图15-4所示:

当第二次更新完成后(更新尾节点指针),队列将再次处于稳定状态,如图15-5所示:

LinkedQueue.put方法在插入新元素之前,将首先检查队列是否处于中间状态(步骤A)。如果是,那么有另一个线程正在插入元素(在步骤C和D)之间。此时当前线程不会等待其他线程执行完成,而是帮助它完成操作,并将尾节点向前推进一个节点(步骤B)。然后,它将重复执行这种检查,以免另一个线程已经开始插入元素,并继续推进尾节点,知道它发现队列处于稳定状态之后,才会开始执行自己的插入操作。

15.4.3 原子域的更新器

原子的域更新器类表示现有volatile域的一种基于反射的“视图”,从而能够在已有的volatile域上使用CAS。在更新器类中没有构造函数,要创建一个更新器对象,可以调用newUpdater工厂方法,并定制类和域的名字。域更新器没有与某个特定的实例关联在一起,因而可以更新目标类的任意实例中的域。它提供的原子保证性比普通的原子类更弱一些。在ConcurrentLinkedQueue,使用nextUpdater的compareAndSet方法来更新Node中的next域,完全是为了提升性能。对于一些频繁分配并且生命周期短暂的对象,如队列中的链接节点,如果能去掉每个Node的AtomicRefrence创建过程,那么将极大地降低插入操作的开销。然而,几乎在所有情况下,普通原子变量的性能都很不错,只有在很少的情况下才会使用原子的域更新器。(如果在执行原子更新的同时还需要维持现有类的串行化形式,那么原子的域更新器将非常有用)

15.4.4 ABA问题

如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。一个简单的解决方案是:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值有A变为B,然后又变为A,版本号也将是不同的。AtomicStampedReference和AtomicMarkableReference支持在两个变量上执行原子的条件更新。


小结:

《java并发编程实战》读书笔记12--原子变量,非阻塞算法,CAS的更多相关文章

  1. Java并发编程实战 读书笔记(一)

    最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一  线程与进程   进程与线程的解释   个人觉 ...

  2. Java并发编程实战 第15章 原子变量和非阻塞同步机制

    非阻塞的同步机制 简单的说,那就是又要实现同步,又不使用锁. 与基于锁的方案相比,非阻塞算法的实现要麻烦的多,但是它的可伸缩性和活跃性上拥有巨大的优势. 实现非阻塞算法的常见方法就是使用volatil ...

  3. Java并发编程实战 读书笔记(二)

    关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...

  4. 《java并发编程实战》笔记

    <java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为:  Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...

  5. Java多线程编程实战读书笔记(一)

    多线程的基础概念本人在学习多线程的时候发现一本书——java多线程编程实战指南.整理了一下书中的概念制作成了思维导图的形式.按照书中的章节整理,并添加一些个人的理解.

  6. 《Java并发编程实战》笔记-非阻塞算法

    如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败和挂起,那么这种算法就被称为非阻塞算法.如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无锁(Lock-Free)算法 ...

  7. 《Java并发编程实战》笔记-锁与原子变量性能比较

    如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...

  8. 《Java并发编程实战》笔记-OneValueCache与原子引用技术

    /** * NumberRange * <p/> * Number range class that does not sufficiently protect its invariant ...

  9. Java并发编程艺术读书笔记

    1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...

  10. Java并发编程实践读书笔记(5) 线程池的使用

    Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...

随机推荐

  1. HDU 2083(排序+绝对值+中间值求和)

    简易版之最短距离 点我跳转到HDOJ Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) ...

  2. Java API不能远程访问linux服务器HBase的问题

    今天我在虚拟机里面安装了Hbase 1.2.4,说在windows上Java API调用访问下玩玩,结果始终连接不上. 现象是启动程序后,程序出现卡死的状态,没报错也不停止,大约半分钟后才打印一堆日志 ...

  3. JS中验证URL、图片

    //验证URL function IsURL (str_url) { var strRegex = '^((https|http|ftp|rtsp|mms)?://)' + '?(([0-9a-z_! ...

  4. Chrome浏览器的使用技巧

    查询Chrome浏览器自身的DNS缓存 在浏览器的地址栏输入:chrome://net-internals/#dns 在左侧的菜单栏,点击对应菜单可以查看对应选项的相关内容. 在Chrome浏览器中输 ...

  5. HDU 5651 逆元

    xiaoxin juju needs help Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/ ...

  6. Elastic Search操作入门

    前言 Elastic Search是基于Lucene这个非常成熟的索引方案,另加上一些分布式的实现:集群,sharding,replication等.具体可以参考我同事写的文章. 本文主要介绍ES入门 ...

  7. windows下codeblocks报错undefined reference to `WSAStartup@8'|

    Windows下C++Socket编程,调用WSAStartup函数报错:undefined reference to `WSAStartup@8'| 本人使用的是Codeblocks MinGW M ...

  8. C11性能之道:右值引用

    1.左值与右值 C++11中新增了一种类型,右值引用,标记为T &&. 首先来介绍什么是左值和右值,左值是指表达式结束后依旧存在的持久对象,而右值是指表达式结束之后就不再存在的临时对象 ...

  9. 「6月雅礼集训 2017 Day5」吃干饭

    [题目大意] 询问[L,R]中选若干个数异或起来得到的答案集合大小.多组数据. 对于50%的数据,$R - L \leq 10^4$ 对于100%的数据,$R - L \leq 10^{18}, T ...

  10. 原生ES-Module在浏览器中的尝试

    其实浏览器原生模块相关的支持也已经出了一两年了(我第一次知道这个事情实在2016年下半年的时候) 可以抛开webpack直接使用import之类的语法 但因为算是一个比较新的东西,所以现在基本只能自己 ...