5.0 KiB
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.
- Operations like
-
Atomic variables:
- Provide built-in thread-safe operations.
- Use low-level CPU instructions (like CAS – Compare-And-Swap).
- No need for explicit synchronization (like
synchronizedor locks).
2 - Atomic classes in java.util.concurrent.atomic
Examples of atomic classes:
AtomicIntegerAtomicLongAtomicBooleanAtomicReference
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
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:
atomicCounterwill always be correct (10000)normalCountermay 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:
-
High contention
- Many threads compete for the same lock
- Threads spend time waiting instead of doing work
-
Excessive synchronization
- Overuse of locks reduces parallelism
- Even independent operations get blocked
-
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:
-
Stress testing
- Run with many threads
- Increase contention
- Repeat tests multiple times
-
Introduce artificial delays
- Add
sleep()between lock acquisitions - Makes timing issues more visible
- Increases probability of deadlock
- Add