## ⚛️ Atomic Variables & Synchronization --- ### **1 -** Atomic variables solve thread-safety problems without using explicit synchronization (synchronized blocks or Lock objects). Their primary purposes are: 1. Provide thread-safe operations on single variables without locks 2. Enable non-blocking algorithms with better performance under moderate contention 3. Ensure visibility across threads (like volatile variables) 4. Support atomic read-modify-write operations (e.g., increment, compare-and-set) ### **2 -** Four Atomic Classes from java.util.concurrent.atomic: 1. **AtomicInteger:** Atomic operations for int values 2. **AtomicLong:** Atomic operations for long values 3. **AtomicBoolean:** Atomic operations for boolean values 4. **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:** ```java 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: ```text -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: 1. **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. 2. **Cache Coherence Traffic (The "Hidden" Contention):** Even lock-free atomic variables generate significant CPU cache traffic that limits scalability. 3. **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. 1. **Context Switching Overhead:** When more threads than CPU cores exist, the OS constantly switches between threads, saving/restoring state. 2. **Contention for Shared Resources:** As threads increase, probability of conflict superlinearly increases. 3. **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. 4. **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: 1. **Low contention in tests:** Threads rarely overlap in lock acquisition 2. **Fast operations:** Critical sections so brief that interleaving is improbable 3. **Small thread pools:** Fewer permutations of lock ordering 4. **Deterministic scheduling:** Same interleaving every run (lulls developers into false safety) 5. **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.