1. Overview
In this tutorial, we'll review details of the AtomicStampedReference class from the java.util.concurrent.atomic package.
After that, we'll explore the AtomicStampedReference classes atomic operations and how we can use it to perform A-B-A detection.
2. Why Do We Need 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.
What does AtomicStampedReference do for us, though, that AtomicReference and AtomicMarkableReference don't already do?
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.
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. This is referred to as the A-B-A Problem.
This can be especially handy in lock-free data structures since they rely heavily on compare-and-swap (CAS) operations. However, it's helpful in any situation when we need to know if a reference has been altered since our last read, like optimistically locking a database record.
3. How Does AtomicStampedReference Help Detect A-B-A?
AtomicStampedReference provides both an object reference and a stamp to indicate if the object reference has been modified. A thread changes both the reference and the stamp atomically.
Now, if another thread modifies the reference from A to B then back to A we can detect this situation because the stamp will have changed.
So, let's see this in action.
3.1. Reading a Value and Its Stamp
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.
To access the balance, we'll want to get both the balance and the stamp. We can achieve this with a non-zero-length array that will hold the stamp:
int holder = new int[1]; int balance = account.get(holder); int stamp = holder[0];
We pass that array to AtomicStampedReference#get to atomically obtain both the current account balance and the stamp.
3.2. Changing a Value and Its Stamp
Now, let's review how to set the value of an AtomicStampedReference atomically.
If we want to change the account's balance, we need to change both the balance and the stamp:
int newStamp = stamp + 1; 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. Thus, we've abstracted away the notion of “latest value” from the reference itself.
Finally, let's encapsulate our account logic into a class:
public class StampedAccount { AtomicInteger stamp = new AtomicInteger(0); AtomicStampedReference<Integer> account = new AtomicStampedReference<>(100, 0); public int getBalance() { return this.account.get(new int[1]); } public int getStamp() { int[] stamps = new int[1]; this.balance.get(stamps); return stamps[0]; } public boolean deposit(int funds) { int[] holder = new int[1]; int balance = this.account.get(holder); int newStamp = this.stamp.incrementAndGet(); return this.account.compareAndSet(balance, balance + funds, holder[0], newStamp); } public boolean withdrawal(int funds) { int[] holder = new int[1]; int current = this.account.get(holder); int newStamp = this.stamp.incrementAndGet(); return this.account.compareAndSet(balance, balance - funds, holder[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:
int[] holder = new int[1]; int balance = this.account.get(holder); 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.
Finally, thread 1 runs:
return this.account.compareAndSet(balance, balance + 100, holder[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.
3.3. Testing
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.withdrawal(100)) Thread.yield(); }); t.start(); Assert.assertTrue(account.deposit(100)); t.join(1_000); Assert.assertFalse(t.isAlive()); Assert.assertSame(0, account.getBalance()); } }
3.4. Choosing the Next Stamp
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.
4. Conclusion
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.
As always, we can find the code available over on GitHub.