## Question 1
An **atomic variable** is a special type of variable that performs operations 

The purpose of using them is to prevent **race conditions**

- **Atomic variables**: thread-safe for simple operations (like increment, update).

- **Ordinary variables**: can cause race conditions if multiple threads change them together without protection.

## Question 2

Examples of classes in java.util.concurrent.atomic:

1. AtomicInteger
2. AtomicLong
3. AtomicBoolean
4. AtomicReference

AtomicInteger:
Used as a thread-safe counter. For example, in a multithreaded server, multiple threads can increase the number of requests safely without using synchronized.

## Question 3

Atomic variables: It's better for simple operations on a single value (increment, update, compare-and-set).
They are usually faster and lighter because they avoid blocking threads.

Locks: Used when multiple operations must happen together. Threads wait until the lock is released.


Use a lock when:

- Multiple variables must stay consistent together

- A complex sequence of actions must be protected

- You need critical sections with more control


Use an atomic variable when:

- Only one variable is shared

- Operations are simple (counter)
 
- High performance and low overhead are important


## Question 4

A program can be free of race conditions but still be slow under high contention because correctness does not mean good performance.
Threads may work safely but spend time waiting instead of doing useful work.

Three factors that limit scalability:

1. Lock Contention
   Many threads try to access the same lock. Only one can continue, so others wait, reducing parallelism.


2. Context Switching
   The CPU constantly switches between threads. Frequent switching adds overhead and wastes time.


3. Synchronization Overhead
   Operations like locks, atomic updates, and coordination between threads require extra CPU work and memory synchronization.


## Question 5

Adding more threads does not always improve performance because after a certain point, threads start spending more time coordinating and competing than doing actual work.

1. When there are too many threads, the CPU frequently switches between them. 
Saving and restoring thread states creates overhead and reduces efficiency.


2. Multiple threads may compete for the same resources (locks, memory, shared data). Some threads must wait, so parallel execution decreases.


3. Each CPU core has its own cache. When several threads modify shared data, caches must stay synchronized,
creating extra communication and slowing execution.


4. Locks, atomic operations, and coordination mechanisms require additional processing time to maintain correctness.


## Question 6

Deadlocks often appear in production but not during testing. During testing, threads may run in a favorable order and 
never reach the lock sequence that causes deadlock. In production, different timing, higher load, and more threads can create the exact conditions needed.

Two strategies to expose deadlocks during testing:

1. Stress / Load Testing
   Run the program with many threads, repeated executions, and high concurrency to increase the chance of problems.


2. Introduce Scheduling Variations
   Add random delays, sleeps, or use concurrency testing tools to force different execution orders and increase the probability of triggering deadlocks.
