Java中的AtomicStampedReference指南

2023/06/07

1. 概述

在之前的文章中,我们了解到AtomicStampedReference可以防止ABA问题

在本教程中,我们将更深入地了解如何最好地使用它。

2. 为什么我们需要AtomicStampedReference?

首先,AtomicStampedReference为我们提供了一个对象引用变量和一个我们可以原子读写的戳记。我们可以将戳记看作时间戳或版本号

简单地说,添加戳记可以让我们检测到另一个线程何时将共享引用从原始引用A更改为新的引用B,然后又更改回原始引用A

让我们看看它在实践中的表现。

3. 银行账户示例

考虑一个有两条数据的银行账户:余额balance和上次修改日期lastModifyDate。每次更改余额时都会更新上次修改日期,通过观察这个上次修改日期,我们可以知道该帐户已经更新。

3.1 读取值及其版本号

首先,让我们假设我们的引用持有账户余额:

AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0);

请注意,我们初始化balance为100,版本号为0。

要访问余额,我们可以使用AtomicStampedReference.getReference()方法。

类似地,我们可以通过AtomicStampedReference.getStamp()获取版本号。

3.2 更改值及其版本号

现在,让我们回顾一下如何以原子方式设置AtomicStampedReference的值。

如果我们想更改账户的余额,我们需要同时更改余额和版本号:

if (!account.compareAndSet(balance, balance + 100, stamp, stamp + 1)) {
    // retry
}

compareAndSet方法返回一个表示成功或失败的布尔值。失败意味着余额或版本号自我们上次读取以来已经发生变化。

正如我们所看到的,使用它们的getter方法很容易获取引用对象和版本号

但是,如上所述,当我们想使用CAS更新它们的值时,我们需要它们的最新版本。要以原子方式检索这两条信息,我们需要同时获取它们。

幸运的是,AtomicStampedReference为我们提供了一个基于数组的API来实现这一点。让我们通过实现Account类的withdraw()方法来演示它的用法:

public boolean withdrawal(int funds) {
    int[] stamps = new int[1];
    int current = this.account.get(stamps);
    int newStamp = this.stamp.incrementAndGet();
    return this.account.compareAndSet(current, current - funds, stamps[0], newStamp);
}

同样,我们可以添加deposit()方法:

public boolean deposit(int funds) {
    int[] stamps = new int[1];
    int current = this.account.get(stamps);
    int newStamp = this.stamp.incrementAndGet();
    return this.account.compareAndSet(current, current + funds, stamps[0], newStamp);
}

我们刚刚写的内容的好处是,我们可以在取款或存款之前知道没有其他线程改变余额,甚至回到我们上次读取后的状态。

例如,考虑以下线程交错执行

余额设置为100,线程1运行deposit(100)直到以下点:

int[] stamps = new int[1];
int current = this.account.get(stamps);
int newStamp = this.stamp.incrementAndGet(); 
// Thread 1 is paused here

这意味着存款尚未完成。

然后,线程2运行deposit(100)和withdraw(100),使余额达到200,然后又回到100。

最后,线程1运行:

return this.account.compareAndSet(current, current + 100, stamps[0], newStamp);

线程1将成功检测到自上次读取以来其他线程已更改了帐户余额,即使余额本身与线程1读取时的余额相同。

3.3 测试

测试起来很棘手,因为这取决于非常具体的线程交错。但是,让我们至少编写一个简单的单元测试来验证存款和取款是否有效:

@Test
void givenMultiThread_whenStampedAccount_thenSetBalance() throws InterruptedException {
    StampedAccount account = new StampedAccount();

    Thread t1 = new Thread(() -> {
        while (!account.deposit(100)) {
            Thread.yield();
        }
    });
    t1.start();

    Thread t2 = new Thread(() -> {
        while (!account.withdrawal(100)) {
            Thread.yield();
        }
    });
    t2.start();

    t1.join(10_000);
    t2.join(10_000);

    assertFalse(t1.isAlive());
    assertFalse(t2.isAlive());

    assertEquals(0, account.getBalance());
    assertTrue(account.getStamp() > 0);
}

3.4 选择下一个版本号

从语义上讲,戳记就像时间戳或版本号,因此它通常总是递增的。也可以使用随机数生成器。

这样做的原因是,如果戳记可以更改为之前的内容,这可能会破坏AtomicStampedReference的目的。AtomicStampedReference本身并没有强制执行此约束,因此我们需要遵循这种做法

4. 总结

总之,AtomicStampedReference是一个功能强大的并发工具类,它提供了可以原子读取和更新的引用和戳记的方式。它是为ABA检测而设计的,应该优先于其他并发类,例如关注ABA问题的AtomicReference。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章