# Ninth Assignment — Answers

---

### ⚛️ Atomic Variables & Synchronization

---

### **1 - What are atomic variables?**

Atomic variables are variables that support **lock-free, thread-safe operations**. This means operations on them (such as increment, compare-and-set, etc.) are performed as a single indivisible step.

Their main purpose is to **prevent race conditions** when multiple threads access and modify shared data concurrently.

#### Difference from ordinary variables:

* **Ordinary variables (non-atomic):**

    * Operations like `x++` are NOT atomic.
    * They consist of multiple steps: read → modify → write.
    * This can lead to race conditions.

* **Atomic variables:**

    * Provide built-in thread-safe operations.
    * Use low-level CPU instructions (like CAS – Compare-And-Swap).
    * No need for explicit synchronization (like `synchronized` or locks).

---

### **2 - Atomic classes in `java.util.concurrent.atomic`**

Examples of atomic classes:

* `AtomicInteger`
* `AtomicLong`
* `AtomicBoolean`
* `AtomicReference`

#### Example use case: `AtomicInteger`

`AtomicInteger` is commonly used as a **thread-safe counter**.

Example:

* Counting number of requests in a web server
* Tracking successful operations across multiple threads

It avoids race conditions without using locks, improving performance under moderate contention.

---

### **3 - Locks vs Atomic Variables**

#### Atomic Variables:

**Advantages:**

* Faster (lock-free)
* No blocking
* Simple for single-variable operations

**Limitations:**

* Only suitable for **simple operations**
* Not useful for multi-step or multi-variable logic

---

#### Locks (`synchronized`, `ReentrantLock`):

**Advantages:**

* Can protect **complex operations**
* Allow coordination across multiple variables
* Support conditions and waiting

**Disadvantages:**

* Slower due to blocking
* Can cause deadlocks if misused

---

#### When to use which?

* Use **Atomic Variables** when:

    * You have simple operations (e.g., increment counter)
    * No need for coordination between multiple variables

* Use **Locks** when:

    * Multiple shared variables are involved
    * You need atomic multi-step operations
    * You need waiting/notification mechanisms

---

### 🎯 Bonus Task — Example Program

```java
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    static int normalCounter = 0;
    static AtomicInteger atomicCounter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        int threads = 10;
        Thread[] workers = new Thread[threads];

        for (int i = 0; i < threads; i++) {
            workers[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    normalCounter++; // NOT thread-safe
                    atomicCounter.incrementAndGet(); // thread-safe
                }
            });
            workers[i].start();
        }

        for (Thread t : workers) {
            t.join();
        }

        System.out.println("Normal Counter: " + normalCounter);
        System.out.println("Atomic Counter: " + atomicCounter.get());
    }
}
```

Expected result:

* `atomicCounter` will always be correct (10000)
* `normalCounter` may be less due to race conditions

---

### 🔒 Locks & Concurrent Design

---

### **4 - Race-free but poor performance**

A program can be completely correct (no race conditions) but still perform poorly due to **high contention and synchronization overhead**.

#### Reasons:

1. **High contention**

    * Many threads compete for the same lock
    * Threads spend time waiting instead of doing work

2. **Excessive synchronization**

    * Overuse of locks reduces parallelism
    * Even independent operations get blocked

3. **Lock granularity issues**

    * Coarse-grained locks (one big lock)
    * Prevent multiple threads from working in parallel

---

### **5 - Why more threads ≠ better performance**

Adding more threads can hurt performance due to:

#### 1. Context Switching

* CPU switches between threads
* This has overhead and wastes time

#### 2. Contention

* Threads compete for shared resources (locks, memory)
* More threads → more waiting

#### 3. Cache Coherence

* CPUs maintain consistency of cached data
* Frequent updates cause cache invalidation
* Slows down execution

#### 4. Synchronization Overhead

* Locks and coordination add extra cost
* More threads → more synchronization

---

### ⚠️ Deadlocks

---

### **6 - Why deadlocks appear in production**

Deadlocks depend on **thread scheduling**, which is:

* Non-deterministic
* Timing-dependent

In testing:

* Fewer threads
* Predictable execution

In production:

* High concurrency
* Different timing → circular waits may occur

---

#### Strategies to expose deadlocks:

1. **Stress testing**

    * Run with many threads
    * Increase contention
    * Repeat tests multiple times

2. **Introduce artificial delays**

    * Add `sleep()` between lock acquisitions
    * Makes timing issues more visible
    * Increases probability of deadlock