QuickCart tracks many small numbers: orders placed today, items sold, failed payment attempts. Priya first used synchronized methods to bump each counter. It worked, but under heavy load threads lined up waiting for the lock. For a single number that only goes up or down, there is a lighter tool.
The problem with synchronized counters
public class OrderStats {
private int ordersToday = 0;
public synchronized void recordOrder() {
ordersToday++; // Safe, but every thread waits here
}
public synchronized int getOrdersToday() {
return ordersToday;
}
}
synchronized works. But each ordersToday++ is really three steps: read the value, add one, write it back. With many threads, they take turns. Lock overhead and context switching add up.
Problems with synchronized for simple counters:
- Lock contention — threads queue up waiting
- Context switching — the OS swaps threads in and out
- Performance bottleneck — one hot counter becomes a choke point
AtomicInteger — a counter with built-in safety
AtomicInteger wraps one int and gives you thread-safe operations without synchronized:
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger ordersToday = new AtomicInteger(0);
// Read and write
ordersToday.set(10);
int value = ordersToday.get();
// Get and set in one step
int oldValue = ordersToday.getAndSet(20); // Returns 20, value is now 20
// Increment and decrement
int newValue = ordersToday.incrementAndGet(); // ++counter, returns new value
int oldValue2 = ordersToday.getAndIncrement(); // counter++, returns old value
int newValue2 = ordersToday.decrementAndGet(); // --counter, returns new value
int oldValue3 = ordersToday.getAndDecrement(); // counter--, returns old value
// Add
int afterAdd = ordersToday.addAndGet(5); // counter += 5, returns new value
int beforeAdd = ordersToday.getAndAdd(5); // returns old, then counter += 5
Priya drops this into QuickCart’s stats dashboard:
public class QuickCartStats {
private final AtomicInteger ordersToday = new AtomicInteger(0);
private final AtomicInteger failedPayments = new AtomicInteger(0);
public void recordOrder() {
ordersToday.incrementAndGet();
}
public void recordFailedPayment() {
failedPayments.incrementAndGet();
}
public int getOrdersToday() {
return ordersToday.get();
}
public int getFailedPayments() {
return failedPayments.get();
}
}
Many threads can call recordOrder() at the same time. No lock. No waiting in line.
CAS in plain English
Atomic classes use CAS — Compare-And-Swap. Think of it like this:
- Read the current value (say, 5).
- Compute what you want (6).
- Tell the CPU: “If the value is still 5, change it to 6. If someone else changed it first, tell me and I will try again.”
No big lock. Threads retry quickly if two workers bump the counter at the exact same instant. For simple counters, this is usually faster than synchronized.
You can see CAS directly:
AtomicInteger counter = new AtomicInteger(10);
// Update only if current value is still 10
boolean success = counter.compareAndSet(10, 20);
// success is true — value is now 20
boolean again = counter.compareAndSet(10, 30);
// again is false — value is still 20, not 10
Use compareAndSet when you need a conditional update — “change this only if nobody else changed it since I looked.”
Update functions — getAndUpdate and friends
Java 8 added functional update methods. They apply your logic atomically:
AtomicInteger counter = new AtomicInteger(10);
// Update and return the new value
counter.updateAndGet(x -> x * 2); // 20
// Return the old value, then update
int old = counter.getAndUpdate(x -> x + 5); // old is 20, new value is 25
// Accumulate with a custom function
counter.accumulateAndGet(10, (current, added) -> current + added); // 35
These are still one atomic step each. Good for counters that need more than a plain increment.
AtomicLong — when int is not enough
Same API as AtomicInteger, but for long values. Use it when counts can exceed Integer.MAX_VALUE (about 2 billion):
import java.util.concurrent.atomic.AtomicLong;
AtomicLong totalRevenueCents = new AtomicLong(0L);
totalRevenueCents.incrementAndGet();
totalRevenueCents.addAndGet(100L);
totalRevenueCents.compareAndSet(100L, 200L);
QuickCart’s lifetime revenue in cents fits better in a long than an int.
AtomicReference — swapping one object safely
Sometimes you hold a reference to an object, not a number. AtomicReference swaps that reference in one CAS step:
import java.util.concurrent.atomic.AtomicReference;
AtomicReference<String> currentPromotion = new AtomicReference<>("None");
currentPromotion.set("10% Off Milk");
boolean updated = currentPromotion.compareAndSet(
"10% Off Milk",
"Free Shipping"
);
String old = currentPromotion.getAndSet("Buy One Get One");
Priya uses it for a config object that gets replaced whole — never edited in place by two threads:
public class StoreConfig {
private final AtomicReference<String> bannerText =
new AtomicReference<>("Welcome to QuickCart");
public void updateBanner(String newText) {
bannerText.set(newText);
}
public String getBanner() {
return bannerText.get();
}
}
For a real config with many fields, prefer an immutable object and swap the whole thing with AtomicReference. Do not mutate the object after putting it in the reference unless you know exactly what you are doing.
Thread-safe one-time setup
AtomicReference with compareAndSet can ensure only one thread creates something:
public class QuickCartSingleton {
private static final AtomicReference<QuickCartSingleton> instance =
new AtomicReference<>();
private QuickCartSingleton() {}
public static QuickCartSingleton getInstance() {
QuickCartSingleton current = instance.get();
if (current == null) {
QuickCartSingleton newInstance = new QuickCartSingleton();
if (instance.compareAndSet(null, newInstance)) {
return newInstance;
} else {
return instance.get();
}
}
return current;
}
}
Only one thread wins the compareAndSet(null, newInstance) race. The others get the instance the winner created.
AtomicBoolean — one-time flags
For simple true/false flags that many threads read and write:
import java.util.concurrent.atomic.AtomicBoolean;
AtomicBoolean saleActive = new AtomicBoolean(false);
saleActive.set(true);
boolean isActive = saleActive.get();
boolean flipped = saleActive.compareAndSet(false, true);
boolean oldFlag = saleActive.getAndSet(false);
One-time initialization — only the first thread runs setup:
public class QuickCartInitializer {
private final AtomicBoolean initialized = new AtomicBoolean(false);
public void initialize() {
if (initialized.compareAndSet(false, true)) {
// Only one thread can execute this block
loadProductCatalog();
connectToPaymentGateway();
System.out.println("QuickCart initialized");
}
}
}
Priya uses this so the payment gateway connects exactly once, even if ten threads call initialize() at startup.
When atomics beat synchronized
Use atomics when:
- You update one variable (one counter, one flag).
- The operation is simple (increment, compare-and-set, add).
- Many threads hit the same variable often.
Stick with synchronized or locks when:
- You must update several variables together as one unit.
- You need a block of code to run alone, not just one number.
// BAD: Two separate atomic steps — not one transaction
ordersToday.incrementAndGet();
revenueToday.addAndGet(price);
// GOOD: Keep related updates inside one synchronized block
synchronized (this) {
ordersToday++;
revenueToday += price;
}
Atomics are for single-variable speed. They are not a replacement for full locking when logic spans multiple fields.
QuickCart: counting sold items per product
Priya combines ConcurrentHashMap with AtomicInteger — one counter per product:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class SalesCounter {
private final ConcurrentHashMap<String, AtomicInteger> sold =
new ConcurrentHashMap<>();
public void recordSale(String product) {
sold.computeIfAbsent(product, k -> new AtomicInteger(0))
.incrementAndGet();
}
public int getSoldCount(String product) {
AtomicInteger counter = sold.get(product);
return counter == null ? 0 : counter.get();
}
}
Each product gets its own atomic counter. Threads rarely fight over the same one unless the same product sells like crazy — which is exactly when you want speed.
Full stats service pattern:
public class QuickCartMetrics {
private final AtomicInteger successCount = new AtomicInteger(0);
private final AtomicInteger failureCount = new AtomicInteger(0);
public void recordSuccess() {
successCount.incrementAndGet();
}
public void recordFailure() {
failureCount.incrementAndGet();
}
public int getSuccessCount() {
return successCount.get();
}
public int getFailureCount() {
return failureCount.get();
}
}
What not to do with atomics
Do not chain two atomic updates and expect them to behave like one transaction:
// BAD: Not atomic as a pair
counter.incrementAndGet();
counter.addAndGet(10);
// GOOD: One synchronized block or one atomic update function
synchronized (this) {
counter.incrementAndGet();
counter.addAndGet(10);
}
Do not use AtomicReference for huge objects that are expensive to swap. Prefer small immutable config objects.
Atomic classes do not give you ordering guarantees across different atomic variables. If thread A writes atomic X and thread B must see it before reading atomic Y, you may still need volatile or synchronized.
What to remember
AtomicIntegerandAtomicLonggive lock-free, thread-safe updates to one number.- CAS means “change this value only if it still equals what I expect.”
getAndUpdate,updateAndGet, andaccumulateAndGetapply functions atomically.- Prefer atomics over
synchronizedfor simple single-variable counters. - Do not use atomics for multi-step logic that must happen together.
AtomicReferencesafely swaps one object reference at a time.AtomicBooleanworks well for flags and one-time initialization withcompareAndSet.
What Priya does next: some objects are not safe to share at all. She gives each worker their own copy with ThreadLocal — like a private sticky note on each desk.