diff --git a/Answers.md b/Answers.md new file mode 100644 index 0000000..f5732e5 --- /dev/null +++ b/Answers.md @@ -0,0 +1,182 @@ +### ⚛️ Atomic Variables & Synchronization + + +1. Atomic variables in Java are part of the `java.util.concurrent.atomic` package. +They provide lock‑free, thread‑safe operations on a single variable. +Their purpose is to allow read‑modify‑write operations to be performed +as a single, indivisible unit without using synchronized blocks or explicit locks. +2. + * `AtomicInteger` – for `int` values + * `AtomicLong` – for `long` values + * `AtomicBoolean` – for `boolean` values + * `AtomicReference` – for object references (any type). +
+ Typical use case for `AtomicInteger`: A request counter in a web server. + Each incoming request increments the counter to generate a unique ID or to count total requests. + Multiple threads (handling requests concurrently) can safely call `incrementAndGet()` without any locks. +3. Locks and atomic variables are both tools for managing thread safety, +but they differ significantly in how they work and when you should use them. +
+ * Locks can protect arbitrarily large sections of code and can coordinate access to multiple + variables simultaneously. For example, if you need to update two bank accounts in a single atomic + operation, you must use a lock. Atomic variables, on the other hand, only cover a single variable. + You cannot atomically update two independent `AtomicInteger` objects together without additional synchronization. + * Locks are blocking – when a thread cannot acquire a lock, it is suspended by the operating system and later + woken up, which causes context switches and overhead. Atomic variables are non‑blocking; they use hardware‑level + Compare‑And‑Swap (CAS) instructions. If an atomic operation fails because another thread modified the variable, + it simply retries immediately (spins) without leaving the CPU. + * Because locks can be held while waiting for other locks, they can cause deadlocks if not used carefully. + Atomic variables never cause deadlocks because there is no waiting for locks – each operation either + succeeds immediately or retries. + * For simple operations like incrementing a counter, atomic variables are usually much faster than locks, + especially when contention is low to moderate. However, under extremely high contention (many threads pounding + the same variable), atomic variables may suffer from excessive retry spinning, and a well‑tuned lock might perform + better. For long critical sections (e.g., many lines of code, I/O, or complex updates), locks are more efficient + because spinning would waste CPU cycles. +* **When is a lock a better choice?** + + -When you need to atomically update multiple variables that belong together (e.g., transferring money between accounts). + + -When the critical section is long or contains blocking operations (like network calls or file I/O). + + -When you need explicit waiting and notification. + +* **When is an atomic variable a better choice?** + + -For simple, single‑variable operations such as counters, sequence generators, or status flags. + + -When you want lock‑free code that cannot deadlock. + + -For high‑frequency updates where lock overhead would become a bottleneck (e.g., statistics collection, request counters). + + + +#### 🎯Bonus Task: +```java +public class raceCondition +{ + private static int plainCounter = 0; + private static AtomicInteger atomicCounter = new AtomicInteger(0); + + public static void main(String[] args) throws InterruptedException + { + final int THREAD_COUNT = 10; + final int INCREMENTS_PER_THREAD = 1000; + + Thread[] threads = new Thread[THREAD_COUNT]; + + for (int i = 0; i < THREAD_COUNT; i++) + { + threads[i] = new Thread(()-> { + for (int j = 0; j < INCREMENTS_PER_THREAD; j++) + { + plainCounter++; + atomicCounter.incrementAndGet(); + } + }); + } + + for (int i = 0;i < THREAD_COUNT; i++) + { + threads[i].start(); + } + for (int i = 0;i < THREAD_COUNT; i++) + { + threads[i].join(); + } + + int expected = THREAD_COUNT*INCREMENTS_PER_THREAD; + System.out.println("expected: "+expected); + System.out.println("int: "+plainCounter); + System.out.println("atomic: "+atomicCounter); + + } +} +``` +sample output: +``` +expected: 10000 +int: 8880 +atomic: 10000 +``` + +--- +### 🔒 Locks & Concurrent Design + +4. **Explanation:** + Even if a program has no race conditions, it can still suffer from poor performance when many threads compete +for the same resources. High contention means many threads try to access the same shared data or locks at the same time. +While correctness is preserved, throughput can drop dramatically because threads spend more time waiting, retrying, +or invalidating caches than doing useful work. + + **Three concurrency‑related factors that limit scalability:** + + * **Lock contention** + + If a program uses a single coarse‑grained lock (e.g., synchronizing the whole method), only one thread can + execute the critical section at a time. All other threads queue up and block. As more threads are added, the queue + grows, but the throughput cannot exceed the rate at which the lock is released and reacquired. This turns a + concurrent program into essentially a sequential one for that resource, creating a scalability bottleneck. + + * **Cache coherence traffic** + + On modern multi‑core CPUs, each core has its own cache. When multiple threads repeatedly read and write to the + same memory location (even with atomic operations), the caches must stay consistent. The hardware uses a cache + coherence protocol. Every write to a shared variable invalidates the cache line in all other cores, forcing them to + reload from main memory or a shared cache. Under high contention, this causes a storm of invalidations and cache + misses, increasing memory latency and reducing performance even without explicit locks. + + * **False sharing** + + False sharing occurs when two or more threads modify different variables that happen to reside on the same + cache line (typically 64 bytes). Although the threads do not share the same logical variable, the cache coherence + protocol treats the entire line as shared. When one thread updates its variable, the cache line is invalidated on + other cores, causing unnecessary reloads. This can slow down seemingly independent threads, and the problem worsens + with more threads because the probability of cache line overlaps increases. + + +5. * **Context switching overhead** + + The operating system can run only as many threads as there are hardware cores (or hardware threads like +Hyper‑Threading). When the number of active threads exceeds the number of cores, the OS must constantly pause one thread +and switch to another. A context switch involves saving and restoring register states, updating memory management +structures, and flushing parts of the pipeline and caches. Each switch costs microseconds – small per switch, but when +thousands of switches happen per second, total overhead becomes significant, reducing useful work throughput. + * **Contention for shared resources** + + As more threads compete for the same locks, memory, or I/O channels, the fraction of time spent waiting + (blocking) increases. Throughput does not increase linearly and eventually saturates. Contention on a popular lock + can cause the system to spend most of its time in the operating system scheduler and in lock‑handling code, leading + to severe performance collapse. + * **Cache coherence** + + More threads mean more cores reading and writing to shared data. Every write to a shared variable triggers cache + coherency traffic (invalidations, bus transactions). This traffic increases with the square of the number of + contending cores in some cases. Moreover, all cores share the same memory bus. When many threads access memory + heavily, the bus becomes a bottleneck, and memory latency increases due to queuing delays. + * **Synchronization overhead** + + Every locking operation (`synchronized`, `ReentrantLock`, `Semaphore`) involves overhead: acquiring the lock, + possibly parking the thread, and later unparking it. Even lock‑free atomic operations under high contention cause + repeated retries, which burn CPU cycles without progressing. The overhead per operation grows, and total throughput can drop. + +--- +### ⚠️ Deadlocks +6. * **Why Deadlocks Often Appear Only in Production? (Thread‑Scheduling Perspective)** + + Deadlocks are notoriously hard to reproduce during testing because they depend on specific interleavings of thread +execution – the exact order in which threads acquire locks. In a testing environment (e.g., with low load, few cores, or +deterministic scheduling), the probability of hitting the exact timing window where two threads hold locks in opposite +order is extremely low. + * **Two Strategies to Expose Deadlocks During Testing:** + * Stress Testing with Thread Interleaving Controllers (e.g., jcstress, Lincheck): + Use tools that systematically explore thread interleavings. For example, Java Concurrency Stress (jcstress) + generates many schedules, including rare ones. Alternatively, ConcurrentLinkedDeque test harnesses or Lincheck + (from Kotlin) can be used. A simpler approach: in a test, repeatedly run a scenario with many threads and use + Thread.yield() or Thread.sleep(1) at strategic points to increase the chance of switching contexts in the middle of + lock acquisition. + * Inject Artificial Delays and Random Preemption Points: + Within the critical sections, insert small random sleeps (Thread.sleep(1)) or Thread.yield() right after + acquiring the first lock but before acquiring the second lock. This greatly increases the chance of interleaving. + Use a randomised test runner that loops the same test thousands of times with different random seeds. Also, run + tests on machines with more CPU cores and under load to make scheduling less predictable.