Simply put, even set() is atomic, when you just finished assigning the value, it already left the "guarding" by the atomicity granted by set() and it's vulnerable for change. Another if (x==y) before or after set() cannot guarantee the value is the one we need when we set().
Imagine this scenario:
You generate some token locally. You want to share this token between threads as much as possible.
At first it's null:
// static make it shared between singleton instances; volatile makes the change visible immediately to all threads static volatile AtomicReference<String> token = new AtomicReference(null);
And when getting token:
public String getToken() { if (token.compareAndSet(null, generateToken())) { // entering here means the comparison done atomically finds the token is null and requested a new one log.debug("Generating new token for the first time"); return token.get(); } // atomic comparison returns false, meaning token already present log.debug("Return already generated token"); return token.get(); }
Some early threads sees the null value, all of them called generateToken(), so they have different token on each thread. But when the 1st thread updates the value, all the other threads will see it. And the later threads will get present token.
When generating token is cheap(locally done, no external API I/O operations), non-lock compareAndSet() ensures higher throughput comparing to a synchronized block. My own tests proven that when there are 100 threads, the difference in time is visible already.
But when generating token is expensive (external API), you may just use synchronized (lock) block to ensure only one thread is doing the HTTP request.
100if and only if the current value is300for example. You cannot read the value and compare it yourself, because during your comparison some other thread could have updated the value.