229 lines
5.0 KiB
Markdown
229 lines
5.0 KiB
Markdown
# 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 |