Contents

AtomicStampedReference 简介

1. 概述

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

在本教程中,我们将仔细研究如何最好地使用它。

2. 为什么我们需要AtomicStampedReference

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

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

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

3. 银行账户示例

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

3.1.读取值及其标记

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

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

请注意,我们提供了余额 100 和邮票 0。 要访问余额,我们可以 对account 成员变量使用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. 测试

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

public class ThreadStampedAccountUnitTest {
    @Test
    public void givenMultiThread_whenStampedAccount_thenSetBalance() throws InterruptedException {
        StampedAccount account = new StampedAccount();
        Thread t = new Thread(() -> {
            while (!account.deposit(100)) {
                Thread.yield();
            }
        });
        t.start();
        Thread t2 = new Thread(() -> {
            while (!account.withdrawal(100)) {
                Thread.yield();
            }
        });
        t2.start();
        t.join(10_000);
        t2.join(10_000);
        assertFalse(t.isAlive());
        assertFalse(t2.isAlive());
        assertEquals(0, account.getBalance());
        assertTrue(account.getStamp() > 0);
    }
}

3.4. 选择下一张戳记

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

这样做的原因是,如果可以将标记更改为以前的标记,这可能会破坏AtomicStampedReference的目的。

AtomicStampedReference本身并不强制执行此约束,因此我们需要遵循这种做法。