4.7 KiB
Theory Answers – Concurrent Banking System
1. Atomic Variables & Synchronization
1.1 What are atomic variables?
Atomic variables are special types that support lock‑free, thread‑safe operations on a single variable. They guarantee that certain compound actions (like read‑modify‑write) are performed as one indivisible step – no other thread can see an intermediate state.
Why do we need them?
In a multi‑threaded environment, a plain int counter shared among threads can produce wrong results because counter++ is actually three separate steps: read, add, write. Without proper synchronization, threads can interleave these steps and lose updates. Atomic variables solve this using low‑level hardware support (e.g., Compare‑And‑Swap, CAS) without the overhead of locks.
How they differ from ordinary variables:
Ordinary variables offer no built‑in guarantees about visibility or atomicity. If multiple threads access them without external synchronization, you get data races and inconsistent values. Atomic variables, on the other hand, ensure that each operation is applied completely before any other thread can see a change – and they also enforce memory visibility (happens‑before relationships).
1.2 Four classes from java.util.concurrent.atomic
AtomicInteger– forintvalues.AtomicLong– forlongvalues.AtomicBoolean– forbooleanflags.AtomicReference<V>– for object references.
Typical use case for AtomicInteger:
Imagine a web server that counts the total number of processed requests. Every worker thread increments this counter after handling a request. Using AtomicInteger with incrementAndGet() guarantees that the final count is always accurate, no matter how many threads are running concurrently, and it does so without blocking.
1.3 Locks vs. Atomic Variables – when to use which?
| Aspect | Atomic Variables | Locks (e.g., synchronized, ReentrantLock) |
|---|---|---|
| Overhead | Very low (CAS is usually a single CPU instruction) | Higher (context switching, queue management) |
| Complexity | Best for simple updates on a single variable | Needed for compound actions across multiple resources |
| Blocking behaviour | Non‑blocking – retry on failure | Blocking – thread waits if lock is held |
| Flexibility | Limited to atomic operations on one variable | Can handle multiple conditions, timeouts, fair ordering |
When a lock is a better choice:
- When you need to update several related variables together (e.g., transfer money between two accounts – both must be locked).
- When you need conditional waiting (e.g., wait until the balance is sufficient).
- When the contention is low and lock overhead is negligible compared to the work done inside the critical section.
When an atomic variable is preferable:
- For simple counters, accumulators, or status flags.
- When you have many threads and you want to avoid blocking and reduce latency.
- When you are building non‑blocking data structures.
🎯 Bonus Task – Demonstration Program
Below is a small Java program that launches multiple threads to increment both a plain int and an AtomicInteger. Run it and you’ll see the plain integer often ends up with a value less than the expected total, while the AtomicInteger always gives the correct result – clearly showing the race condition.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static int plainCounter = 0;
private static AtomicInteger atomicCounter = new AtomicInteger(0);
private static final int THREADS = 10;
private static final int INCREMENTS_PER_THREAD = 1000;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < INCREMENTS_PER_THREAD; j++) {
plainCounter++; // Not atomic
atomicCounter.incrementAndGet(); // Atomic
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("Plain counter final value: " + plainCounter);
System.out.println("Atomic counter final value: " + atomicCounter.get());
// Expected: 10000 (10 * 1000)
}
}