6.0 KiB
⚛️ Atomic Variables & Synchronization
1 - Atomic variables solve thread-safety problems without using explicit synchronization (synchronized blocks or Lock objects). Their primary purposes are:
-
Provide thread-safe operations on single variables without locks
-
Enable non-blocking algorithms with better performance under moderate contention
-
Ensure visibility across threads (like volatile variables)
-
Support atomic read-modify-write operations (e.g., increment, compare-and-set)
2 - Four Atomic Classes from java.util.concurrent.atomic:
-
AtomicInteger: Atomic operations for int values
-
AtomicLong: Atomic operations for long values
-
AtomicBoolean: Atomic operations for boolean values
-
AtomicReference: Atomic operations for object references
- A use case is when a web server needs to generate unique, sequential request IDs across thousands of concurrent threads.
3 - When to use…
-
Atomic Variables: when you're updating only one variable with simple operations under moderate contention.
-
Locks: when you need to coordinate multiple resources, have complex conditions, or require fairness or blocking semantics.
| Aspect | Locks (synchronized, ReentrantLock) | Atomic Variables |
|---|---|---|
| Mechanism | Blocking (threads wait/park) | Non-blocking (CAS operations) |
| Scope Can | protect multiple variables/operations | Single variable only |
| Overhead | Higher (context switching, OS involvement) | Lower (CPU-level instructions) |
| Contention handling | Threads block and may be descheduled | Threads retry without blocking |
| Deadlock risk | Yes (especially with multiple locks) | No |
| Fairness options | Available (e.g., ReentrantLock(true)) | No (CAS is inherently unfair) |
| Composite operations | Easy (multiple steps together) | Difficult or impossible |
- Bonus:
import java.util.concurrent.atomic.AtomicInteger;
public class example {
private static int count1;
private static AtomicInteger count2 = new AtomicInteger(0);
public example() {}
public static void increment() { count1++; }
public static void atomicIncrement() { count2.incrementAndGet(); }
public static void main(String args[]) throws InterruptedException {
Runnable r = () -> {
for (int i = 1; i <= 1000; i++) {
atomicIncrement();
increment();
}
};
Thread t1 = new Thread(r, "thread-1");
Thread t2 = new Thread(r, "thread-2");
Thread t3 = new Thread(r, "thread-3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("-int- variable value: " + count1 + " -AtomicInteger- variable value: " + count2);
}
}
Output:
-int- variable value: 2971 -AtomicInteger- variable value: 3000
🔒 Locks & Concurrent Design
4 - A program can be free of race conditions through proper synchronization (locks, atomic variables, concurrent collections), but still suffer from:
-
Contention: Threads competing for the same resources
-
Over-synchronization: Too much or too coarse-grained locking
-
Hardware limitations: Cache coherence traffic, false sharing
Concurrency-related factors that may limit scalability even when correctness is guaranteed:
-
Lock Contention (Serialization): Even with correct locking, if threads frequently wait for locks, execution becomes effectively serialized. Threads queue up despite having multiple CPU cores.
-
Cache Coherence Traffic (The "Hidden" Contention): Even lock-free atomic variables generate significant CPU cache traffic that limits scalability.
-
False Sharing: When threads modify different variables that accidentally share the same CPU cache line, the cache coherence protocol treats them as if they were the same variable.
5 - This is the scalability ceiling problem. Adding threads eventually hurts performance due to coordination overhead.
-
Context Switching Overhead: When more threads than CPU cores exist, the OS constantly switches between threads, saving/restoring state.
-
Contention for Shared Resources: As threads increase, probability of conflict superlinearly increases.
-
Cache Coherence Traffic: Modern CPUs maintain cache coherence (MESI protocol). When multiple cores modify shared data, they generate coherence traffic that doesn't exist with fewer threads.
-
Synchronization Overhead (Locking): Even without contention, acquiring/releasing locks has intrinsic overhead.
⚠️ Deadlocks
6 - A deadlock requires four conditions:
-
Mutual exclusion: Resources can't be shared
-
Hold and wait: Thread holds one resource while waiting for another
-
No preemption: Resources can't be forcibly taken
-
Circular wait: Threads form a cycle of dependencies
But even with all conditions present, a deadlock only occurs when threads acquire locks in precisely the wrong order at precisely the wrong time. Specific scheduling factors that hide deadlocks:
-
Low contention in tests: Threads rarely overlap in lock acquisition
-
Fast operations: Critical sections so brief that interleaving is improbable
-
Small thread pools: Fewer permutations of lock ordering
-
Deterministic scheduling: Same interleaving every run (lulls developers into false safety)
-
No GC pressure: GC can pause threads at vulnerable moments
Strategy 1 : Manual Lock Order Inversion (Eclipse Contest) Force the circular wait condition by deliberately inverting lock order in tests.
Strategy 2 : Inject Preemptive Scheduling Points (ThreadWeaver) Force thread context switches at vulnerable points using controlled test frameworks like ThreadWeaver or JCStress.