Contents

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 操作适用于三个操作数:

  1. 操作的内存位置 (M)
  2. 变量的现有期望值 (A)
  3. 需要设置的新值(B)

CAS 操作将 M 中的值原子更新到 B,但前提是 M 中的现有值与 A 匹配,否则不采取任何操作。

在这两种情况下,都会返回 M 中的现有值。这将三个步骤(获取值、比较值和更新值)组合到单个机器级别的操作中。

当多个线程尝试通过 CAS 更新同一个值时,其中一个线程获胜并更新该值。然而,与锁不同的是,没有其他线程被挂起;相反,他们只是被告知他们没有设法更新该值。然后线程可以继续进行进一步的工作,并且完全避免了上下文切换。

另一个后果是核心程序逻辑变得更加复杂。这是因为我们必须处理 CAS 操作不成功的情况。我们可以一次又一次地重试它,直到它成功,或者我们可以什么都不做,根据用例继续前进。

4. Java 中的原子变量

Java 中最常用的原子变量类是AtomicIntegerAtomicLongAtomicBooleanAtomicReference 。这些类分别代表一个intlongboolean和 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。