# 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