@@@ 在 java.util.concurrent 包的许多类中,例如 Semaphore 和 ConcurrentLinkedQueue ,都提供
了比 synchronized 机制更高的性能和可伸缩性。这种性能提升的主要来源:原子变量与非阻塞的同步机制。
@@@ 非阻塞算法:
---------- 这种算法用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性。
---------- 非阻塞算法被广泛地用于在操作系统和 JVM 中实现线程 / 进程调度机制 、 垃圾回收机制以及锁和
其他并发数据结构。
@@@ 与基于锁的方案相比,非阻塞算法在设计和实现上都要复杂得多,但它在可伸缩性和活跃性问题上
却拥有巨大的优势。
@@@ 在非阻塞算法中不存在死锁和其他活跃性问题。
@@@ 非阻塞算法不会受到单个线程失败的影响。
@@@ 从 Java 5.0 开始,可以使用原子变量类(例如 AtomicInteger 和 AtomicReference)来构建高效的
非阻塞算法。
@@@ 原子变量提供了与 volatile 类型变量相同的内存语义,此外还支持原子的更新操作,从而使它们更加
适用于实现计数器 、 序列发生器和统计数据收集等,同时还能比基于锁的方法提供更高的可伸缩性。
》》锁的劣势
@@@ 现代的许多 JVM 都对非竞争锁获取和锁释放等操作进行了极大的优化,但如果有多个线程同时
请求锁,那么 JVM 就需要借助操作系统的功能。如果出现了这种情况,那么一些线程将被挂起并且在稍后
恢复运行。
@@@ 与锁相比,volatile 变量是一种更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换
或线程调度等操作。然而,volatile 变量同样存在一些局限:虽然它们提供了相似的可见性保证,但不能用于
构建原子的复合操作。因此,当一个变量依赖其他的变量时,或者当变量的新值依赖于旧值时,就不能使用
volatile 变量。
@@@ 锁还存在其他一些缺点:
--------- 当一个线程正在等待锁时,它不能做任何其他事情。
@@@ 锁定方式对于细粒度的操作(例如递增计数器)来说仍然是一种高开销的机制。在管理线程之间的
竞争时应该有一种粒度更细的技术,类似于 volatile 变量的机制,同时还要支持原子的更新操作。幸运的是,
在现代的处理器中提供了这种机制。
》》硬件对并发的支持
@@@ 在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。
@@@ 现在,几乎所有的现代处理器中都包含了某种形式的原子读----改----写指令,例如比较并交换
(Compare-and-Swap)或者关联加载 / 条件存储(Load-Linked / StoreConditional)。
操作系统和 JVM 使用这些指令来实现锁和并发的数据结构,但在 Java 5.0 之前,在 Java 类中
还不能直接使用这些指令。
### 比较并交换
@@@ 在大多数处理器架构(包括 IA32 和 Sparc)中采用的方法是实现一个比较并交换(CAS)指令。
(在其他处理器中,例如 PowerPC ,采用一对指令来完成相同的功能:关联加载和条件存储)。
@@@ CAS 包含了 3 个操作数-------需要读写的内存位置 V 、 进行比较的值 A 、拟写入的新值 B
-------- 当且仅当 V 的值等于 A 时,CAS 才会通过原子方式用新值 B 来更新 V 的值,否则不会执行任何
操作。
-------- 无论位置 V 的值是否等于 A ,都将返回 V 原有的值。(这种变化被称为比较并设置,无论操作
是否成功都会返回)。
@@@ 当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他
线程都将失败。然而,失败的线程并不会被挂起(这与获取锁的情况不同:当获取锁失败时,线程将被
挂起),而是被告知这次竞争中失败。
@@@ 由于一个线程在竞争 CAS 时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复
操作,也或者不执行任何操作。
@@@ CAS 的典型使用模式是:首先从 V 中读取值 A ,并根据 A 计算新值 B , 然后再通过 CAS 以
原子方式将 V 中的值由 A 变成 B (只要在这期间没有任何线程将 A 的值修改为其他值)。由于 CAS 能
检测到来自其他线程的干扰,因此即使不使用锁也能够实现原子的读---改---写操作序列。
### 非阻塞的计数器
@@@ 通常,反复地重试是一种合理的策略,但在一些竞争很激烈的情况下,更好的方式是在重试之前
首先等待一段时间或者回退,从而避免造成活锁问题。
@@@ 在实际情况中,如果仅需要一个计数器或序列生成器,那么可以直接使用 AtomicInteger 或
AtomicLong ,它们能提供原子的递增方法以及其他算术方法。
@@@ 由于 CAS 在大多数情况下都能成功执行(假设竞争程度不高),因此硬件能够正确地预测 while
循环中的分支,从而把复杂控制逻辑的开销降至最低。
@@@ 虽然 Java 语言的锁定语法比较简洁,但 JVM 和操作在管理锁时需要完成的工作却并不简单。在
实现锁定时需要遍历 JVM 中一条非常复杂的代码路径,并可能导致操作系统级的锁定 、 线程挂起以及
上下文切换等操作。
@@@ CAS 的主要缺点是:它将使调用者处理竞争问题(通过重试 、 回退 、 放弃),而在锁中能自动
处理竞争问题(线程在锁之前将一直阻塞)。
@@@ CAS 的性能会随着处理器数量的不同而变化很大。
@@@ CAS 的执行性能不仅在不同体系架构之间变化很大,甚至在相同处理器的不同版本之间也会发生
改变。
@@@ 一个很管用的经验法则是:在大多数处理器上,在无竞争的锁获取和释放的 “ 快速代码路径 ” 上的
开销,大约是 CAS 开销的两倍。
### JVM 对 CAS 的支持
@@@ 在 Java 5.0 中引入了底层的支持,在 int 、 long 和对象的引用等类型上都公开了 CAS 操作,
并且 JVM 把它们编译为底层硬件提供的最有效方法。
----------- 在支持 CAS 的平台上,运行时把它们编译为相应的(多条)机器指令。
----------- 在最坏的情况下,如果不支持 CAS 指令,那么 JVM 将使用自旋锁。
----------- 在原子变量类(例如 java.util.concurrent.atomic 中的 AtomicXxx)中使用了底层 JVM 支持,为
数字类型和引用类型提供一种高效的 CAS 操作,而在 java.util.concurrent 中的大多数类在实现时
则直接或间接地使用了原子变量类。
》》原子变量类
@@@ 原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常
关键的。
@@@ 在使用基于原子变量而非锁的算法中,线程在执行时更不易出现延迟,并且如果遇到竞争,也更容易
恢复过来。
@@@ 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读--改---写操作。
@@@ AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,
因为它直接利用了硬件对并发的支持。
@@@ 共有 12 个原子变量类,可分为 4组:标量类(Scalar)、更新器类 、数组类 、 复合变量类。
-------- 最常用的原子变量就是标量类:AtomicInteger 、 AtomicLong 、AtomicBoolean 、AtomicReference 。
-------- 原子数组类(只支持 Integer 、 Long 和 Reference 版本)中的元素可以实现原子更新。原子数组类
为数组的元素提供了 volatile 类型的访问语义,这是普通数组所不具备的特性。
@@@ 基本类型的包装类是不可修改的,而原子变量类是可修改的。
### 原子变量是一种 “ 更好的 volatile ”
### 性能比较:锁与原子变量
@@@ 伪随机数字生成器(PRNG)
@@@ 在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实的情况下,原子变量的性能
将超过锁的性能。这是因为锁在发生竞争时会挂起线程,从而降低 CPU 的使用率和共享内存总线上的同步
信号量。
@@@ 任何一个真实的程序都不会除了竞争锁或原子变量,其他什么工作都不做。在实际情况中,原子
变量在可伸缩性上要高于锁,因为在应对常见的竞争程度时,原子变量的效率会更高。
@@@ 在中低程度的竞争下,原子变量能够提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效
地避免竞争。
》》非阻塞算法
@@@ 如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被
称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法也被称为无锁
(Lock--Free)算法。
@@@ 如果在算法中仅将 CAS 用于协调线程之间的操作,并且能正确地实现,那么它既是一种无阻塞
算法,又是一种无锁算法。
@@@ 在非阻塞算法中通常不会出现死锁和优先级反转问题(但可能会出现饥饿和活锁问题,因为在
算法中会反复地重试)。
@@@ CasCounter 是一种非阻塞算法。
在许多常见的数据结构中都可以使用非阻塞算法,包括栈 、 队列 、 优先队列以及散列表等。
### 非阻塞的栈
@@@ 在实现相同功能的前提下,非阻塞算法通常比基于锁的算法更为复杂。
@@@ 创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单个变量上,同时还要维护
数据的一致性。
@@@ 栈是最简单的链式数据结构:每个元素仅指向一个元素,并且每个元素也只被一个元素引用。
@@@ 非阻塞算法的所有特性:某项工作的完成具有不确定性,必须重新执行。
### 非阻塞的链表
@@@ 构建非阻塞算法的技巧在于:将执行原子修改的范围缩小到单个变量上。
@@@ 链接队列比栈更为复杂,因为它必须支持对头节点和尾节点的快速访问。
@@@ 在为链接队列构建非阻塞算法时,可以使用技巧:即使在一个包含多个步骤的更新操作中,
也要确保数据结构总是处于一致的状态。
还可以使用其他技巧:如果当 B 线程到达时发现 A 线程正在修改数据结构,那么在数据结构
中应该有足够多的信息,使得 B 线程能完成 A 线程的更新操作。如果 B 线程 “帮助” A 线程完成了更新
操作,那么 B 可以执行自己的操作,而不用等待 A 的操作完成。当 A 恢复后再试图完成其操作时,会
发现 B 线程已经替它完成了。
@@@ 在许多队列算法中,空队列通常都包含一个 “ 哨兵(Sentinel)节点 ” 或者 “ 哑(Dummy)节点 ” ,
并且头节点和尾节点在初始化时都指向该哨兵节点。尾节点通常要么指向哨兵节点(如果队列为空),即
队列的最后一个元素,要么(当有操作正在执行更新时)指向倒数第二个元素。
@@@ LinkedQueue.put 方法在插入新元素之前,将首先检查队列是否处于中间状态。如果是,
那么另一个线程正在插入元素。
### 原子的域更新器
@@@ 在 ConcurrentLinkedQueue 中没有使用原子引用来表示每个 Node ,而是使用普通的 volatile
类型引用,并通过基于反射的 AtomicReferenceFieldUpdater 来进行更新。
@@@ 原子域更新器类表示现有的 volatile 域的一种基于反射的 “ 视图 ” ,从而能够在已有的 volatile
域上使用 CAS 。
在更新器类中没有构造函数,要创建一个更新器对象,可以调用 newUpdater 工厂方法,并制定
类和域的名字。
域更新器类没有与某个特定的实例关联在一起,因而可以更新目标类的任意实例中的域。
@@@ 在 ConcurrentLinkedQueue 中,使用 nextUpdater 的 compareAndSet 方法来更新 Node 的
next 域。这个方法有点繁琐,但完全是为了提升性能。
@@@ 在几乎所有情况下,普通原子变量的性能都很不错,只有在很少的情况下才需要使用原子的
域更新器。(如果在执行原子更新的同时还需要维持现有类的串行化形式,那么原子的域更新器将
非常有用)。
### ABA 问题
@@@ ABA 问题是一种异常现象:如果在算法中的节点可以被循环使用,那么在使用 “ 比较并交换 ”
指令时就可能出现这种问题(主要在没有垃圾回收机制的环境中)。
@@@ 在某些算法中,如果 V 的值首先由 A 变成 B ,再由 B 变成 A ,那么仍然被认为是发生了
变化,并需要重新执行算法中的某些步骤。
@@@ 如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现 ABA 问题。
@@@ 如果通过垃圾回收器来管理链表节点仍然无法避免 ABA 问题,那么还有一个相对简单的
解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值
由 A 变为 B , 然后又变成 A ,版本号也将是不同的。
@@@ AtomicStampedReference 将更新一个 “ 对象---引用 ” 二元组,通过在引用上加上 “ 版本号 ”,
从而避免 ABA 问题。
@@@ AtomicMarkableReference 将更新一个 “ 对象引用----布尔值 ” 二元组,在某些算法中将通过
这种二元组使节点保存在链表中同时又将其标记为 “ 已删除的节点 ” 。
》》小结
@@@ 非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层
的原语通过原子变量类向外公开,这些类也用做一种 “ 更好的 volatile 变量 ” ,从而为整数和对象引用
提供原子的更新操作。
@@@ 非阻塞算法在设计和实现时非常困难,但通常能够提供更高的可伸缩性,并能更好地防止活跃
性故障的发生。在 JVM 从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(在
JVM 内部以及平台类库中)对非阻塞算法的使用。