15.1 锁的劣势

与锁相比,volatile 变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换或线程调度等操作。然而,volatile变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于构建原子的复合操作。因此,当一个变量依赖其他的变量时,或者当变量的新值依赖于旧值时,就不能使用volatile 变量。这些都限制了volatile 变量的使用,因此它们不能用来实现一些常见的工具,例如计数器或互斥体(mutex)。

15.2硬件对并发的支持

独占锁是一项悲观技术一它假设最坏的情况(如果你不锁门,那么捣蛋鬼就会闯入并搞得一团糟),并且只有在确保其他线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去。
乐观的方法:
可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试(也可以不重试)。

15.2.1 比较并交换 - SimulatedCAS

CAS包含了3个操作数一一需要读写的内存位置 V、进行比较的值A和拟写人的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。(这种变化形式被称为比较并设置,无论操作是否成功都会返回。)
CAS的含义是:“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。
CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另一个线程在最近一次检查后更新了该变量,那么CAS能检测到这个错误。程序清单15-1中的SimulatedCAS说明了CAS语义(而不是实现或性能)。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同:当获取锁失败时,线程将被挂起),而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。这种灵活性就大大减少了与锁相关的活跃性风险(尽管在一些不常见的情况下仍然存在活锁风险一请参见10.3.3 节)。CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B (只要在这期间没有任何线程将V的值修改为其他值)。由.于CAS能检测到来自其他线程的干扰,因此即使不使用锁也能够实现原子的读-改一写操作序列。

15.2.2 非阻塞的计时器 - CASCounter

事实上,CAS最大的缺陷在于难以围绕着CAS正确地构建外部算法。

15.2.3 JVM对CAS的支持

Java5.0之前,需要编写代码实现CAS,Java5.0引入了底层的支持,在int、long 和对象的引用等类型.上都公开了CAS操作,并且JVM把它们编译为底层硬件提供的最有效方法。在支持CAS的平台上,运行时把它们编译为相应的(多条)机器指令。上,在最坏的情况下,如果不支持CAS指令,那么JVM将使用自旋锁。

15.3 原子变量类

原子变量比锁的粒度更细,量级更轻。原子变量将发生竞争的范围缩小到单个变量上。
共有12个原子变量类,可分为4组:标量类(Scalar)、 更新器类、数组类以及复合变量类。最常用的原子变量就是标量类: AtomicInteger. AtomicLong、 AtomicBoolean 以及AtomicReference.所有这些类都支持CAS,此外,AtormicInteger 和AtomicLong还支持算术运算。(要想模拟其他基本类型的原子变量,可以将short或byte等类型与int类型进行转换,以及使用floatToIntBits或doubleToLongBits来转换浮点数。)
尽管原子的标量类扩展了Number类,但并没有扩展一些基本类型的包装类,例如Integer或Long。事实上,它们也不能进行扩展:基本类型的包装类是不可修改的,而原子变量类是可修改的。在原子变量类中同样没有重新定义hashCode或equals方法,每个实例都是不同的。与其他可变对象相同,它们也不宜用做基于散列的容器中的键值。

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

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

如果能够避免使用共享状态,那么开销将会更小。我们可以通过提高处理竞争的效率来提高可伸缩性,但只有完全消除竞争,才能实现真正的可伸缩性。

15.4 非阻塞算法 - 这一块的内容仅做了解吧(除了15.4.4 ABA问题),以后再回顾

15.4.1 非阻塞的栈

15.4.2 非阻塞的链表

15.4.3 原子的域更新器

15.4.4 ABA问题 - 面试重点

在CAS操作中将判断“V的值是否仍然为A ?”,并且如果是的话就继续执行更新操作。在大多数情况下,包括本章给出的示例,这种判断是完全足够的。然而,有时候还需要知道“自从上次看到V的值为A以来,这个值是否发生了变化?”在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。
如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。在这种情况下,即使链表的头节点仍然指向之前观察到的节点,那么也不足以说明链表的内容没有发生改变。如果通过垃圾回收器来管理链表节点仍然无法避免ABA问题,那么还有一个相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,然后又变为A,版本号也将是不同的。
AtomicStampedReference( 以及AtomicMarkableReference)支持在两个变量上执行原子的条件更新。AtomicStampedReference将更新一个“对象一引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题。类似地,AtomicMarkableReference 将更新-一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的节点”。
—-个人理解: 在CAS中,存在一个比较并交换的操作,步骤如下:

  1. 获取值
  2. 将刚刚获取的值进行比较看看是不是最新值
  3. 如果是最新值,则CAS将会成功,否则CAS将会失败

问题会发生在第1步到第2步之间。
第一步获取值,拿到值为A,在进行第二步之前,其他线程将值修改成B,在此又将B修改成A,好,第二步开始比较,发现当前值还是A,ok,是最新值,CAS将会成功。那么这里明明修改了,但是程序却没有检测出来,这就是ABA问题。解决方案上文的书中也给出了,增加一个版本号即可。

小节

非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用做一种“更好的volatile变量”,从而为整数和对象引用提供原子的更新操作。
非阻塞算法在设计和实现时非常困难,但通常能够提供更高的可伸缩性,并能更好地防止活跃性故障的发生。在JVM从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(在JVM内部以及平台类库中)对非阻塞算法的使用。