# Answers of questions

## ⚛️ Atomic Variables & Synchronization:

### 1:
**Atomic variables** are special variables which
enable thread-safe updates to single variables without the high overhead of explicit locking mechanisms (like **synchronized blocks** or **ReentrantLock**).
the entire operation happens as a single, indivisible unit of work without interruption from other threads. 
unlike normal variables they prevent **race condition** and ensuring that changes made by one thread are immediately visible to all other threads.
Ordinary variables require explicit locks to be thread-safe. Atomic variables rely on hardware-level Compare-And-Swap (CAS) CPU instructions, which are low-level, non-blocking operations.


### 2:

- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference

##### Typical Use Case for AtomicLong:
AtomicLong is frequently used to implement a global transaction ID generator or a high-throughput request counter in web servers.

>Example: Every time a new user request or bank transaction enters the system, the server calls atomicLong.incrementAndGet(). Because this operation is thread-safe and lock-free, thousands of concurrent requests can safely receive a unique, sequential ID without blocking each other or causing performance bottlenecks.

### 3:

#### When a Lock is a better choice:
- State Interdependency: When you need to update multiple variables together as a single atomic unit (e.g., updating both a sourceAccount and a targetAccount balance during a transfer).


- Complex Business Logic: When the state change involves conditions or complex calculations that cannot be expressed as a simple single-value swap.


- High Contention over Long Operations: If threads need to hold onto a resource for a relatively long duration, blocking other threads is cleaner than letting them waste CPU cycles retrying.

#### When an Atomic Variable is a better choice:

- Single-Variable Mutations: Counters, sequence generators, state flags, or simple status trackers.


- Low-to-Moderate Contention: When collisions between threads are infrequent, atomic variables are significantly faster because they avoid the overhead of putting threads to sleep and waking them back up.

## 🔒 Locks & Concurrent Design

### 4: 

This situation occurs because **correctness does not guarantee efficiency**. A program can be 100% correct by over-synchronizing (e.g., using a single global lock for the entire application). While this eliminates race conditions by forcing serial execution, it introduces severe performance bottlenecks.

Three concurrency-related factors that limit scalability are:

- **Lock Contention:** When multiple threads frequently attempt to acquire the same lock simultaneously, most of them are blocked, forced into a waiting state. This serializes execution, stripping away the benefits of a multi-core processor.


- **Thread Amdahl's Law (Serial Fraction):** Scalability is bounded by the sequential portion of the program. If critical sections (locked code) take up a substantial percentage of runtime, adding more processors yields diminishing returns.


- **Context Switching Overhead:** When threads compete for a pessimistic lock, the operating system must repeatedly pause thread execution (saving CPU registers) and resume other threads. This constant switching consumes massive CPU cycles without doing actual application work.

### 5:

Adding more threads past the optimal threshold (usually proportional to the number of physical CPU cores) harms performance due to diminishing physical hardware resources and architectural bottlenecks:


- **Context Switching:** When the number of active threads exceeds the number of physical CPU cores, the OS must schedule threads using time-slicing. The overhead of constantly saving and loading thread states begins to eclipse actual execution time.


- **Contention:** More threads mean more active competitors for shared memory resources, queues, and locks. Threads spend more time waiting in queues than doing productive work.


- **Cache Coherence (Cache Thrashing):** Modern CPUs use L1/L2/L3 caches. When multiple threads across different cores modify the same shared variables, the cores must continuously invalidate each other's caches to keep memory uniform. This results in heavy memory-bus traffic and slow reads directly from the main RAM.


- **Synchronization Overhead:** Managing internal thread structures, lock acquisition queues, and tracking atomic states introduces computational overhead that expands non-linearly with thread count.

## ⚠️ Deadlocks

### 6:

In testing environments, the workload is often lower, the timing uniform, and the execution faster. The OS thread scheduler non-deterministically chooses which thread runs based on thread priorities and CPU availability. In simple test environments, threads often finish their tasks completely before the next thread even reaches the critical section, masking potential interleaving defects. Production introduces unpredictable delays (network latency, disk I/O, heavy load), which change thread interleaving patterns and trigger the cyclic dependencies.

#### Strategies to expose deadlocks during testing:

- Introduce Artificial Delays (Thread.sleep() or Thread.yield()): Injecting randomized short sleeps or yields inside critical sections—right after acquiring the first lock but before acquiring the second—forces alternative thread interleaving schedules and exposes timing vulnerabilities.


- High-Concurrency Stress Testing with Shuffled Inputs: Write automated test suites that spawn hundreds of threads performing inverse operations simultaneously (e.g., half the threads transfer A -> B and the other half transfer B -> A). Randomizing the transaction pairings under massive parallel load drastically increases the statistical probability of a cyclic lock conflict occurring.

