# Ninth Assignment — Advanced Multithreading

## Theoretical Questions

---

## 1. What are atomic variables?

Atomic variables are variables that support operations which happen as one indivisible step. This means that other threads cannot observe the operation in a partially completed state.

They are used to safely update simple shared values without using explicit locks. For example, `counter++` on a normal `int` is not atomic because it includes reading, incrementing, and writing the value back. If multiple threads do this at the same time, some updates may be lost.

Using `AtomicInteger` solves this problem:
atomicCounter.incrementAndGet();

text

This operation is atomic, so concurrent increments are handled correctly.

Ordinary variables do not protect against race conditions, while atomic variables provide thread-safe operations such as increment, decrement, and compare-and-set.

---

## 2. Name at least four classes from the `java.util.concurrent.atomic` package

Some classes from this package are:

- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference<T>
- AtomicIntegerArray
- AtomicLongArray

A common use case for `AtomicInteger` is a thread-safe counter. For example:
AtomicInteger requestCount = new AtomicInteger(0);

requestCount.incrementAndGet();

text

This avoids lost updates when many threads update the counter concurrently.

---

## 3. Compare locks with atomic variables

Atomic variables are useful for simple operations on one shared value, such as counters, flags, or ID generators. They are lightweight and usually do not require blocking threads.

Locks are better when the operation is more complex or involves multiple shared variables. For example, a bank transfer must subtract money from one account and add it to another as one atomic operation. A lock can protect the whole critical section.

Atomic variables are best for simple independent updates, while locks are better for multi-step operations or when several shared resources must remain consistent.

---

## Bonus Task: AtomicInteger vs normal int

The bonus task is implemented in:

`src/main/java/dev/banking/bonus/AtomicCounterDemo.java`

This program creates multiple threads. Each thread increments both a normal `int` counter and an `AtomicInteger`.

The normal `int` may produce an incorrect final value because `normalCounter++` is not atomic. Multiple threads can read the same value and overwrite each other’s updates.

`AtomicInteger.incrementAndGet()` performs the increment atomically, so its final value should match the expected number of increments.

---

## 4. A program is completely free of race conditions but still performs poorly under high contention

A program can be correct but still slow. Being free of race conditions guarantees correctness, but not good performance.

High contention happens when many threads frequently compete for the same shared resource. For example, using one global lock for all bank accounts prevents race conditions but allows only one operation at a time.

Factors that limit scalability include:

- Lock contention
- Context switching
- Cache coherence overhead
- Synchronization overhead

So even correct synchronization can perform poorly if many threads compete for the same resource.

---

## 5. Why adding more threads does not always improve performance

Adding more threads improves performance only when there is enough independent work to run in parallel. If threads mostly wait for shared resources, performance may decrease.

Main reasons include:

- Context switching between many threads
- Contention for locks or shared resources
- Cache coherence overhead between CPU cores
- Synchronization overhead from locks and atomic operations

Therefore, more threads do not automatically mean better performance.

---

## 6. Deadlocks often only appear in production, not during testing

Deadlocks depend on thread scheduling and timing. During testing, the exact order of operations that causes a deadlock may not happen.

For example, one thread may lock account A and wait for account B, while another thread locks account B and waits for account A.

In production systems there are more users, transactions, and CPU cores, which increases the number of possible thread schedules and makes rare deadlocks more likely.

Two ways to detect deadlocks during testing are:

- Stress testing with many threads and operations
- Artificial delays to increase the chance of problematic interleavings

Other helpful methods include repeated tests, randomized workloads, timeouts, and thread dumps using tools like `jstack`.