Java中原子变量简介
1. 简介
简而言之,当涉及并发时,共享的可变状态很容易导致问题。如果对共享可变对象的访问没有得到妥善管理,应用程序很快就会变得容易出现一些难以检测的并发错误。
在本文中,我们将重新审视使用锁来处理并发访问,探讨与锁相关的一些缺点,最后介绍原子变量作为替代方案。
2. 锁具
让我们看一下这个类:
public class Counter {
int counter;
public void increment() {
counter++;
}
}
在单线程环境的情况下,这非常有效;然而,一旦我们允许多个线程写入,我们就会开始得到不一致的结果。
这是因为简单的自增操作(counter++),它可能看起来像一个原子操作,但实际上是三个操作的组合:获取值、自增和写回更新的值。
如果两个线程同时尝试获取和更新值,可能会导致更新丢失。
管理对对象的访问的方法之一是使用锁。这可以通过在增量方法签名中使用synchronized关键字来实现。synchronized关键字确保一次只有一个线程可以进入方法(要了解有关锁定和同步的更多信息,请参阅 - Java 中的同步关键字指南 ):
public class SafeCounterWithLock {
private volatile int counter;
public synchronized void increment() {
counter++;
}
}
此外,我们需要添加volatile关键字以确保线程之间正确的引用可见性。
使用锁解决了这个问题。但是,性能受到打击。
当多个线程尝试获取锁时,其中一个会获胜,而其余线程要么被阻塞,要么被挂起。
暂停然后恢复线程的过程非常昂贵,并且影响系统的整体效率。
在像counter这样的小程序中,上下文切换所花费的时间可能会远远超过实际代码执行的时间,从而大大降低了整体效率。
3. 原子操作
有一个研究分支专注于为并发环境创建非阻塞算法。这些算法利用诸如比较和交换 (CAS) 之类的低级原子机器指令来确保数据完整性。
典型的 CAS 操作适用于三个操作数:
- 操作的内存位置 (M)
- 变量的现有期望值 (A)
- 需要设置的新值(B)
CAS 操作将 M 中的值原子更新到 B,但前提是 M 中的现有值与 A 匹配,否则不采取任何操作。
在这两种情况下,都会返回 M 中的现有值。这将三个步骤(获取值、比较值和更新值)组合到单个机器级别的操作中。
当多个线程尝试通过 CAS 更新同一个值时,其中一个线程获胜并更新该值。然而,与锁不同的是,没有其他线程被挂起;相反,他们只是被告知他们没有设法更新该值。然后线程可以继续进行进一步的工作,并且完全避免了上下文切换。
另一个后果是核心程序逻辑变得更加复杂。这是因为我们必须处理 CAS 操作不成功的情况。我们可以一次又一次地重试它,直到它成功,或者我们可以什么都不做,根据用例继续前进。
4. Java 中的原子变量
Java 中最常用的原子变量类是AtomicInteger 、AtomicLong 、AtomicBoolean 和AtomicReference 。这些类分别代表一个int、long、boolean和 object 引用,它们可以被原子更新。这些类公开的主要方法是:
- get() – 从内存中获取值,以便其他线程所做的更改可见;相当于读取一个volatile变量
- set() – 将值写入内存,以便其他线程可以看到更改;相当于写一个volatile变量
- lazySet() – 最终将值写入内存,可能会通过后续相关的内存操作重新排序。一个用例是为了垃圾收集而使引用无效,永远不会再访问它。在这种情况下,通过延迟 null volatile写入可以实现更好的性能
- compareAndSet() - 与第 3 节中描述的相同,成功时返回 true,否则返回 false
- weakCompareAndSet() – 与第 3 节中描述的相同,但在某种意义上较弱,它不会创建发生前的排序。这意味着它可能不一定会看到对其他变量的更新。从 Java 9开始,该方法 已在所有原子实现中被弃用,取而代之的是 weakCompareAndSetPlain() 。*weakCompareAndSet()的记忆效应 很简单,但它的名字暗示了易失性记忆效应。为了避免这种混淆,他们弃用了这种方法,并添加了四种具有不同记忆效果的方法,例如weakCompareAndSetPlain()*或 weakCompareAndSetVolatile()
使用AtomicInteger实现的线程安全计数器如下例所示:
public class SafeCounterWithoutLock {
private final AtomicInteger counter = new AtomicInteger(0);
public int getValue() {
return counter.get();
}
public void increment() {
while(true) {
int existingValue = getValue();
int newValue = existingValue + 1;
if(counter.compareAndSet(existingValue, newValue)) {
return;
}
}
}
}
如您所见,我们重试compareAndSet操作并在失败时再次尝试,因为我们希望保证对increment方法的调用始终将值增加 1。