Java Atomic

2025年5月3日 | 阅读7分钟

在 Java 中,**原子变量**和**操作**用于并发。**多线程**环境在统一**并发**时会带来问题。共享实体(如对象和变量)在程序执行期间可能会被更改。因此,它们可能导致程序不一致。所以,在并发访问时照顾好共享实体非常重要。在这种情况下,**原子变量**可以解决这个问题。在本节中,我们将结合示例讨论**原子类、原子变量、原子操作**。

在继续本节之前,请确保您了解 Java 中的**线程**、**同步**和**锁**。

Java 原子类

Java 提供了 **java.util.concurrent.atomic** 包,其中定义了原子类。原子类在单个变量上提供**无锁**和**线程安全**的环境或编程。它还支持原子操作。所有原子类都具有 get() 和 set() 方法,这些方法作用于 volatile 变量。这些方法与 volatile 变量的读写方式相同。

该包提供了以下原子类

Class描述
AtomicBoolean用于原子地更新布尔值。
AtomicInteger用于原子地更新整数值。
AtomicIntegerArray一个整数数组,其元素可以原子地更新。
AtomicIntegerFieldUpdater一个基于反射的实用工具,可实现对指定类指定的 volatile int 字段的原子更新。
AtomicLong用于原子地更新长整型值。
AtomicLongArray一个长整型数组,其元素可以原子地更新。
AtomicLongFieldUpdater一个基于反射的实用工具,可实现对指定类指定的 volatile long 字段的原子更新。
AtomicMarkableReferenceAtomicMarkableReference 维护一个对象引用以及一个可原子更新的标记位。
AtomicReference可原子地更新的对象引用。
AtomicReferenceArray一个对象引用数组,其元素可以原子地更新。
AtomicReferenceFieldUpdater一个基于反射的实用工具,可实现对指定类指定的 volatile 引用字段的原子更新。
AtomicStampedReferenceAtomicStampedReference 维护一个对象引用以及一个可原子更新的整数“戳”。
DoubleAccumulator一个或多个变量,它们共同维护一个双精度运行值,该值使用提供的函数进行更新。
DoubleAdder一个或多个变量,它们共同维护一个初始值为零的双精度总和。
LongAccumulator一个或多个变量,它们共同维护一个长整型运行值,该值使用提供的函数进行更新。
LongAdder一个或多个变量,它们共同维护一个初始值为零的长整型总和。

这些类的对象分别代表 int、long、boolean 和对象引用的原子变量。原子类有一些通用方法如下

方法描述
set()用于设置值。
get()用于获取当前值。
lazySet()最终设置为给定的值。
compareAndSet如果当前值 == 预期值,则原子地将值设置为给定的更新值。

原子操作

那些总是同时执行的操作称为**原子操作**或**原子动作**。所有原子操作要么有效地同时发生,要么根本不发生。与 Java 中的原子操作相关的**三个**关键概念如下

1. 原子性处理哪些操作以及一组操作具有**不可见性**。例如,考虑以下代码片段

在上面的代码中,并发运行 increment() 和 decrement() 的行为是**未定义**且**不可预测**的。

2. 可见性决定一个线程的效果何时可以被另一个线程**看到**。例如,考虑以下代码片段

在上面的代码中,即使线程 T1 设置 done 为 true,线程 T2 也可能永远不会停止。另外,请注意线程之间没有同步。

3. 排序决定一个线程中的操作相对于另一个线程的顺序。

字段 a 和 b 在线程 T2 中出现的顺序可能与它们在线程 T1 中设置的顺序不同。

让我们通过一个例子来理解。

在上面的代码片段中,我们声明了一个 int 类型变量 **count**,并在 incrementCount() 方法中将其设置为 1。在这种情况下,要么全部发生,要么根本不发生。因此,它表示一个**原子操作**,该操作称为**原子性**。

让我们看另一个代码片段。

看起来它也是一个原子操作,但事实并非如此。它是一个线性操作,由读、修改、写三个操作组成。因此,它可以部分执行。但如果在多线程环境中使用上述代码,会产生问题。

假设我们在单线程环境中调用了上述代码,count 的更新值为 2。如果我们通过两个单独的线程调用上述方法,它们都会同时访问变量并同时更新 count 的值。为了避免这种情况,我们使用原子操作。

Java 支持几种类型的原子操作,如下所示

  • volatile 变量
  • 低级原子操作(不安全)
  • 原子类

让我们看看如何创建一个原子操作。

原子变量

原子变量允许我们对变量执行原子操作。原子变量最小化了同步,并有助于避免内存一致性错误。因此,它确保了同步。

原子包提供了以下五个原子变量

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicIntegerArray
  • AtomicLongArray

原子变量的必要性

让我们考虑以下代码。

Counter.java

输出

4

上述程序如果在单线程环境中执行,会给出预期的输出。多线程环境可能导致意外的输出。其原因在于,当两个或多个线程同时尝试更新值时,它们可能无法正确更新。

Java 提供了**两种**解决方案来克服这个问题

  • 使用锁和同步
  • 使用原子变量

让我们创建一个 Java 程序并使用原子变量来克服这个问题。

使用原子变量

AtomicExample.java

输出

4

同步 vs. 原子 vs. Volatile

同步AtomicVolatile
仅适用于方法。仅适用于变量。也仅适用于变量。
它除了原子性之外还确保了可见性。它除了原子性之外还确保了可见性。它确保可见性,但不确保原子性。
我们无法实现相同。我们无法实现相同。它存储在 RAM 中,因此访问 volatile 变量速度很快。但它不提供线程安全和同步。
它可以实现为同步块或同步方法。我们无法实现相同。我们无法实现相同。
它可以锁定同一个类对象或不同的类对象。我们无法实现相同。我们无法实现相同。