在并发编程的世界里,原子性操作是保证数据一致性和线程安全的关键。Java在java.util.concurrent.atomic包中提供了一系列原子操作类,它们利用底层硬件平台的CAS(Compare-And-Swap)操作来实现非阻塞的原子性更新操作,从而避免了在并发情境下使用同步的开销。
这些原子类提供了一种机制,使得某些数据结构(如计数器、标记、引用等)在多线程环境中能被线程安全地读取和写入。这些类主要分为五类:基本数据类型、对象引用、对象属性更新器、数组以及累加器类型的原子类。
在这篇文章中,我们将一步步深入研究这些原子类,理解它们的工作原理和适用场景,并通过具体的代码示例来演示它们的使用。
上面的例子展示了一个简单的线程安全计数器,我接下来会分别深入介绍每一类原子操作。
在并发环境中,基本数据类型的原子类提供了一种线程安全的方式来执行简单的算术和逻辑操作。 Java的原子包提供了一系列原子操作类,这里,我们将重点关注三个基本类型:AtomicBoolean, AtomicInteger, AtomicLong。
- AtomicBoolean 用于布尔值的原子操作。
- AtomicInteger 和 AtomicLong 分别用于整型和长整型数值的原子操作。
这些类内部主要利用了 volatile 变量和 CAS 操作来实现线程安全的更新操作。volatile 关键字保证了变量的可见性,而 CAS 保证了更新操作的原子性。
当我们在并发编程中处理对象引用时,AtomicReference 类是Java提供的核心工具之一。它能够保证对对象引用的原子更新操作,使得共享对象在多线程中的访问和修改成为可能而不引起数据竞争。
在这个 SharedResourceAccessor 类中,我们使用 AtomicReference 来封装一个共享资源。通过 compareAndSet 方法,我们能够在检测到期望值时安全地执行更新操作,这对于实现无锁的算法和数据结构至关重要。
在上述代码中,我们使用 AtomicReferenceFieldUpdater 为 Candidate 类的 score 字段创建一个原子更新器,这使我们能够原子地更新 score 字段的值。
Java中提供的原子数组类使我们能够在多线程环境中对数组的单个元素进行原子更新操作。这在你需要一个线程安全的计数器数组或者当你在多个线程中收集数据并希望立即可见更新时特别有用。
以上代码展示了如何创建一个线程安全的原子整型数组。每个数组元素可以被单独地、原子地增加和检索,非常适合于统计和计数场景。
随着并发编程需求的不断发展,JDK并发包也提供了一些特殊用途的原子类,其中累加器就是一种非常实用的工具,专为高性能的累积计算设计。
LongAdder 和 LongAccumulator 是在 JDK 8 中引入的两个累加器类,它们在高并发环境下比 AtomicLong 更为高效,特别是当更新操作远多于读取操作时。
LongAdder 可以被视为一组变量的和,每个变量都独立更新,从而减少了热点,提高了性能。当需要得到总和时,它会计算所有变量的总和返回。
LongAccumulator 提供了更为通用的功能,它除了可以增加之外,还可以定义自己的累加逻辑。
下面是一个使用 LongAdder 的简单例子:
HitCounter 类使用 LongAdder 为网页点击量进行计数。由于 increment 是非阻塞的,可以非常快速地由多个线程并行更新。
LongAdder 和 LongAccumulator 的背后有一个名为 Striped64 的类,它是实现这两个类高性能特性的关键。Striped64 内部使用了一个延迟初始化的原子数组,并利用了低争用的散列技术(称为分条锁),来减少不同线程间潜在的竞争。
理论学习之后,实战案例可以帮助我们更好地理解和应用原子类。在这个章节,我们会通过一些具体的例子来探讨原子类在实际编程中的应用。
计数器是并发编程中最常见的用例之一。下面,我们将展示如何使用 AtomicInteger 来创建一个简单的线程安全计数器。
在这个例子中,通过 incrementAndGet 方法,我们在多线程环境中安全且高效地增加了计数器的值。这个方法屏蔽了复杂的同步控制,代码也更加清晰易懂。
在某些高并发的系统中,原子类的使用可以显著提升性能。例如,一个统计实时数据的Web服务可能会遇到高速写入的情景。在这种场景下,LongAdder 比 AtomicLong 更适合累加操作。下面是一个改进示例:
LongAdder 提供了更小的延迟和更高的吞吐量,这在系统遭遇到极端并发时尤为重要。
理解并发原子类的底层,就不得不提到CAS操作,这是实现高效并发控制的关键原理之一。
CAS即比较并交换(Compare-And-Swap),它是一种无锁的非阻塞算法,其核心思想是当且仅当内存位置的值符合预期值时,才会自动将该位置数据更新为新值。
Java中的原子操作类大多是通过调用 sun.misc.Unsafe 类中提供的CAS相关的本地方法来实现这些操作。这些操作通常在单个操作步骤中检查变量当前的值,并在当前值与预期相符时更新它。
下面是一个模拟CAS操作的高阶视角例子:
在实际的原子类中,这些操作是通过JNI接口调用底层汇编指令实现的,从而确保了操作的原子性。
Unsafe 是 Java 中不为人知却非常强大的一个类,它可以直接操作底层内存,执行类似指针的操作,并且可以执行CAS等原子操作。虽然其方法不应该在常规的Java代码中使用,但Java平台的很多构建块,包括原子类,都是依赖 Unsafe 类的能力构建的。
这段代码展示了 Unsafe 类如何在一个Java类中被用来进行CAS操作。我们可以看到,这里涉及到了直接操作内存的行为,这也解释了为什么 Unsafe 应当慎用。
在使用CAS进行并发控制时,我们可能会遇到一个非常特殊的问题——ABA问题。这个问题发生在一个值从A变成B,然后又变回A,CAS操作感知不到这个变化。
在这段代码中,compareAndSet 方法除了要检查当前值是否等于期望值,还要检查当前戳记是否符合预期。这确保了即使中间状态发生了改变,只要版本号不同,CAS操作仍然会失败。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/9157.html