Java 中的 Atomic vs. Volatile

2025 年 1 月 6 日 | 阅读 5 分钟

Java 提供了一个强大的并发框架,允许开发人员编写高效且安全的**多线程应用程序**。在众多工具和概念中,**原子类 (atomic classes)** 和 **volatile 关键字**对于确保线程安全和共享变量的可见性至关重要。在本节中,我们将讨论 Java 中 atomic 和 volatile 的区别。它们可以显著影响并发应用程序的设计和性能。

Volatile 关键字

Java 中的 volatile 关键字用于将一个变量标记为存储在主内存中。更正式地说,它确保对 volatile 变量的每一次读取都将从主内存中读取,对 volatile 变量的每一次写入都将写入到主内存中。

Volatile 关键字的关键特性

可见性 (Visibility):volatile 变量的主要用途是确保可见性。当一个变量被声明为 volatile 时,它保证任何读取该变量的线程都会看到其他线程对该变量的最新写入。

原子性 (Atomicity):volatile 不保证原子性。像增加变量这样的操作不是原子性的。例如,表达式 `volatileVar++` 不是原子的,因为它涉及到读取当前值、加一、并将结果写回,这个过程可能被其他线程插入。

排序 (Ordering):volatile 变量建立了一个“先行发生 (happens-before)”关系。这意味着在一个线程中写入 volatile 变量之前发生的所有动作,都将“先行发生”于另一个线程中对该 volatile 变量的任何后续读取。这确保了操作的**部分排序**,提供了内存一致性的一些保证。

何时使用 Volatile 关键字

标志和信号 (Flags and Signals):Volatile 非常适合用作标志或信号的变量,我们希望确保始终读取到最新的值。

配置设置 (Configuration Settings):如果我们有一个可以在运行时更改且需要被多个线程频繁读取的配置设置。

简单计数器 (Simple Counters):对于不需要原子性(除了单个读或写操作)的简单计数器。

原子变量 (Atomic Variables)

`java.util.concurrent.atomic` 包提供了一组类,支持对单个变量的**无锁线程安全编程**。这些类提供了对变量的原子操作,确保了可见性和原子性。

原子变量的关键特性

原子性 (Atomicity):原子类的主要特点是支持原子操作。这意味着像增加值这样的读-修改-写操作被保证是原子的。例如,`AtomicInteger` 提供了 `incrementAndGet()` 方法,该方法可以原子地增加值并返回结果。

可见性 (Visibility):与 volatile 变量一样,原子变量也确保了可见性。对原子变量所做的任何更改都会立即对其他线程可见。

无锁算法 (Lock-Free Algorithms):原子类使用低级并发原语,例如**比较并交换 (Compare-And-Swap, CAS)**,在不使用锁的情况下实现线程安全。与传统的锁定机制相比,它在**高竞争场景**下能带来更好的性能。

常见的原子类

AtomicInteger:提供对 `int` 值的原子操作。

AtomicLong:提供对 `long` 值的原子操作。

AtomicBoolean:提供对 `boolean` 值的原子操作。

AtomicReference:提供对对象引用的原子操作。

何时使用原子变量?

计数器 (Counters):原子变量非常适合需要原子递增和递减操作的计数器。

状态管理 (State Management):以线程安全的方式管理状态转换。

乐观并发控制 (Optimistic Concurrency Control):在希望避免锁的开销,而改用 CAS 操作来实现线程安全的场景中。

比较 Volatile 和 Atomic

差异基础VolatileAtomic
原子性不保证原子性。保证特定操作的原子性。
可见性确保跨线程的更改可见性。确保跨线程的更改可见性。
性能对于简单的读/写操作通常更快。由于 CAS 操作,开销略高。
使用场景复杂性适用于更简单的使用场景。适用于需要原子性的更复杂场景。
同步依赖内存屏障来实现可见性。使用 CAS 等低级并发原语。
示例变量`volatile boolean flag;``AtomicInteger counter = new AtomicInteger(0);`
内存一致性建立“先行发生”关系。也建立“先行发生”关系。
常见用例标志、简单的状态变量。计数器、状态转换、无锁算法。
无锁操作不能是的
线程干扰复合操作容易发生线程干扰。通过原子方法防止线程干扰。
锁定机制不使用锁。无锁,避免了传统的锁定机制。
实现复杂度对于基本的可见性需求,实现更简单。由于 CAS 和原子操作,实现稍微复杂一些。

使用场景复杂性

Volatile:适用于**仅需要确保可见性而不需要原子性**的更简单场景。

Atomic:适用于需要**原子读-修改-写操作**的更复杂场景。

示例 - 使用 Volatile 关键字

在此示例中,`flag` 变量被声明为 `volatile`,以确保一个线程对其所做的更改对其他线程立即可见。

示例 - 使用 Atomic

在此示例中,使用 `AtomicInteger` 来确保递增操作是原子的,从而防止使用普通 `int` 和 `volatile` 可能出现的**竞态条件**。

性能考虑

在 volatile 和 atomic 之间进行选择时,性能可以是一个关键考虑因素。由于内存屏障的开销低于原子变量使用的 CAS 操作,因此对于简单的标志和状态检查,volatile 变量通常性能更好。

然而,对于计数器或其他需要原子性的变量,原子变量在高并发争用下通常性能更好,因为它们避免了可能引入显著开销并降低可扩展性的锁定机制。

volatile 和 atomic 变量都在确保 Java 并发模型中的线程安全和可见性方面发挥着关键作用。选择哪一个取决于你应用程序的具体需求。Volatile 适用于只需要确保可见性而不需要原子性的简单场景,而原子变量则非常适合需要原子操作且没有锁开销的场景。理解两者的细微差别可以帮助你设计出更高效、更可靠的多线程应用程序。

在**标志、简单的状态变量以及只需要可见性而不需要原子性**的场景中使用 volatile。

在**计数器、状态转换以及需要原子读-修改-写操作**的情况下使用原子类。

通过恰当利用这些工具,我们可以提高并发 Java 应用程序的性能和可维护性。