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

             @@@  在 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 内部以及平台类库中)对非阻塞算法的使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小达人Fighting

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值