Guide to AtomicStampedReference in Java – Java中的AtomicStampedReference指南

最后修改: 2020年 4月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In a previous article, we learned that AtomicStampedReference can prevent the ABA problem.

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

In this tutorial, we’ll get a closer look at how to best use it.

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

2. Why Do We Need AtomicStampedReference?

2.为什么我们需要AtomicStampedReference

First, AtomicStampedReference provides us with both an object reference variable and a stamp that we can read and write atomically. We can think of the stamp a bit like a timestamp or a version number.

首先,AtomicStampedReference为我们提供了一个对象引用变量和一个我们可以原子式读写的印章我们可以把邮票想得有点像时间戳或版本号

Simply put, adding a stamp allows us to detect when another thread has changed the shared reference from the original reference A, to a new reference B, and back to the original reference A.

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

Let’s see how it behaves in practice.

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

3. Bank Account Example

3.银行账户实例

Consider a bank account that has two pieces of data: a balance and a last modified date. The last modified date is updated any time the balance is altered. By observing this last modified date, we can know the account has been updated.

考虑到一个银行账户有两个数据:一个余额和一个最后修改日期。最后修改日期在余额被改变的时候被更新。通过观察这个最后修改日期,我们可以知道该账户已经被更新。

3.1. Reading a Value and Its Stamp

3.1.读取一个值和它的戳记

First, let’s imagine that our reference is holding onto an account balance:

首先,让我们想象一下,我们的参照物正抓着一个账户余额。

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

Note that we’ve supplied the balance, 100, and a stamp, 0.

请注意,我们已经提供了余额100,和一个印章0。

To access the balance, we can use the AtomicStampedReference.getReference() method on our account member variable.

为了访问余额,我们可以使用AtomicStampedReference.getReference()方法来访问我们的account成员变量。

Similarly, we can obtain the stamp via AtomicStampedReference.getStamp().

同样地,我们可以通过AtomicStampedReference.getStamp()获得邮票。

3.2. Changing a Value and Its Stamp

3.2.改变一个值和它的印记

Now, let’s review how to set the value of an AtomicStampedReference atomically.

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

If we want to change the account’s balance, we need to change both the balance and the stamp:

如果我们想改变账户的余额,我们需要同时改变余额和印章。

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

The compareAndSet method returns a boolean indicating success or failure. A failure means that either the balance or the stamp has changed since we last read it.

compareAndSet方法返回一个布尔值,表示成功或失败。失败意味着自我们上次读取后,余额或印章发生了变化。

As we can see, it’s easy to retrieve the reference and the stamp using their getters.

正如我们所看到的,使用它们的获取器很容易检索到引用和印章。

But, as mentioned above, we need the latest version of them when we want to update their values using the CAS. To retrieve those two pieces of information atomically, we need to fetch them at the same time.

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

Luckily, AtomicStampedReference provides us an array-based API to achieve this. Let’s demonstrate its usage by implementing the withdrawal() method for our Account class:

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

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);
}

Similarly, we can add the deposit() method:

同样地,我们可以添加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);
}

The nice thing about what we’ve just written is we can know before withdrawing or depositing that no other thread has altered the balance, even back to what it was since our last read.

我们刚才写的东西的好处是,我们可以在提款或存款之前知道没有其他线程改变了余额,甚至回到了我们上次阅读后的余额。

For example, consider the following thread interleaving:

例如,考虑以下的线程交错:

The balance is set to $100. Thread 1 runs deposit(100) up to the following point:

余额被设置为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

meaning the deposit has not yet completed.

意味着存款尚未完成。

Then, Thread 2 runs deposit(100) and withdraw(100), bringing the balance to $200 and then back to $100.

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

Finally, Thread 1 runs:

最后,线程1运行。

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

Thread 1 will successfully detect that some other thread has altered the account balance since its last read, even though the balance itself is the same as it was when Thread 1 read it.

线程1将成功地检测到其他一些线程自其最后一次读取后改变了账户余额,即使余额本身与线程1读取时相同。

3.3. Testing

3.3.测试

It’s tricky to test since this depends on a very specific thread interleaving. But, let’s at least write a simple unit test to verify that deposits and withdrawals work:

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

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. Choosing the Next Stamp

3.4.选择下一个邮票

Semantically, the stamp is like a timestamp or a version number, so it’s typically always increasing. It’s also possible to use a random number generator.

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

The reason for this is that, if the stamp can be changed to something it was previously, this could defeat the purpose of AtomicStampedReference. AtomicStampedReference itself doesn’t enforce this constraint, so it’s up to us to follow this practice.

这样做的原因是,如果邮票可以被改成以前的东西,这可能会违背AtomicStampedReference的目的。AtomicStampedReference本身并没有强制执行这个约束,所以要由我们来遵循这个做法。

4. Conclusion

4.总结

In conclusion, AtomicStampedReference is a powerful concurrency utility that provides both a reference and a stamp that can be read and updated atomically. It was designed for A-B-A detection and should be preferred to other concurrency classes such as AtomicReference where the A-B-A problem is a concern.

总之,AtomicStampedReference是一个强大的并发工具,它同时提供了一个可以原子地读取和更新的引用和印章。它是为A-B-A检测而设计的,应该优先于其他并发类,如AtomicReference,因为A-B-A问题是一个值得关注的问题。

As always, we can find the code available over on GitHub.

一如既往,我们可以在GitHub上找到可用的代码