## ⚛️ 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<V>**: 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.