AtomicStampedReference 指南-Java快速进阶教程
1. 概述
在之前的一篇文章中,我们了解到AtomicStampedReference可以防止ABA问题。
在本教程中,我们将仔细研究如何最好地使用它。
2. 为什么我们需要AtomicStampedReference?
首先,AtomicStampedReference为我们提供了一个对象引用变量和一个我们可以原子读写的标记。我们可以将stamp视为时间戳或版本号。
简而言之,添加戳记允许我们检测另一个线程何时将共享引用从原始引用 A 更改为新的引用 B,然后再更改回原始引用 A。
让我们看看它在实践中的行为。
3. 银行账户示例
考虑一个包含两条数据的银行账户:余额和上次修改日期。每次更改余额时,都会更新上次修改日期。通过观察此最后修改日期,我们可以知道该帐户已更新。
3.1. 读取值及其标记
首先,让我们假设我们的参考对象持有一个账户余额:
AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100,0);
注意,我们提供了 balance:100,stamp: 0.。
要访问余额,可以在帐户成员变量上使用atomicstampereference . getreference()方法。
类似地,我们可以通过atomicstampereference . getstamp()获取。
3.2. 更改值及其标记
现在,让我们回顾一下如何以原子方式设置AtomicStampedReference的值。
如果我们想更改帐户的余额,我们需要更改balance
和stamp
:
if (!account.compareAndSet(balance, balance + 100, stamp, stamp + 1)) {
// retry
}
方法返回一个布尔值,指示成功或失败。失败意味着自我们上次阅读以来,balance或
stamp
发生了变化。
如我们所见,使用它们的获取器很容易检索reference及stamp。
但是如上所述,当我们想要使用 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 运行存款 (100) 直到以下点:
int[] stamps = new int[1];
int current = this.account.get(stamps);
int newStamp = this.stamp.incrementAndGet();
// Thread 1 is paused here
这意味着存款尚未完成。
然后,线程 2 运行存款 (100) 和提款 (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. 选择下一个Stamp
从语义上讲,戳就像时间戳或版本号,因此它通常总是在增加。也可以使用随机数生成器。
这样做的原因是,如果可以将Stamp
更改为以前的样子,这可能会破坏AtomicStampedReference的目的。AtomicStampedReference本身并不强制实施此约束,因此我们有责任遵循这种做法。
4. 结论
总之,AtomicStampedReference是一个强大的并发实用程序,它提供了可以原子读取和更新的引用和标记。它是为 A-B-A 检测而设计的,应该优先于其他并发类,例如AtomicReference,其中 A-B-A 问题是一个问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~