# Answers

## Question 1 — Atomic Variables

Atomic variables are variables that support thread-safe operations without using locks.
When multiple threads try to do something like `i++` on a normal variable, this operation
actually has three steps: read, add, write. Another thread can jump in between these steps
and cause wrong results. Atomic variables do this whole thing in one unbreakable step using
a CPU instruction called CAS (Compare And Swap), so no thread can interrupt in the middle.

## Question 2 — Atomic Classes

Four classes from java.util.concurrent.atomic:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference

Use case for AtomicInteger:
Counting how many transactions have been processed across multiple threads.
Instead of putting a lock around a simple counter, we can call
`atomicCounter.incrementAndGet()` which is faster and doesn't block threads.

## Question 3 — Locks vs Atomic Variables

Atomic variables are better when we only need to update a single variable safely,
like a counter or a flag. They are faster because they don't block other threads.

Locks are better when we need to update multiple variables together and all of them
must change as one unit. For example in a bank transfer, we need to subtract from one
account and add to another — both must happen together, so we use locks.

## Question 4 — Good Correctness but Bad Performance

A program can be completely correct with no race conditions but still be slow because:

1. Lock contention: if many threads want the same lock at the same time, they all have
   to wait in line. Only one runs while the rest are stuck doing nothing.

2. False sharing: two threads might be working on different variables, but those variables
   are stored next to each other in CPU cache. When one thread changes its variable, the CPU
   forces the other thread to reload its cache even though nothing it cares about changed.

3. Coarse-grained locking: using one big lock for everything means even operations that
   have nothing to do with each other have to wait. For example depositing into account A
   and account B could happen at the same time, but a global lock prevents that.

## Question 5 — More Threads Doesn't Always Mean Faster

- Context switching: when there are more threads than CPU cores, the OS keeps switching
  between them. Each switch takes time and does zero useful work.

- Contention: more threads fighting over the same lock means longer wait times.
  At some point adding threads just makes the queue longer, not the work faster.

- Cache coherence: each CPU core has its own cache. When a shared variable changes,
  all cores must update their copy. With many threads on many cores, this communication
  becomes a bottleneck.

- Synchronization overhead: every lock and unlock has a cost. With too many threads,
  the time spent on synchronization can be more than the actual work being done.

## Question 6 — Deadlocks in Production but Not in Testing

Deadlocks need a very specific order of events to happen. Thread1 must lock A at exactly
the moment Thread2 locks B, and then both try to get each other's lock. In testing,
the system is usually under low load with few threads, so this exact timing almost never
occurs. In production with thousands of concurrent threads, the chances of hitting that
exact bad sequence become much higher.

Two strategies to expose deadlocks during testing:

1. Stress testing: run tests with a very large number of threads doing random transactions
   at the same time. The more threads competing, the higher the chance of triggering the
   exact bad interleaving that causes a deadlock.

2. Inserting Thread.sleep() inside critical sections during tests: this artificially
   slows down the thread right in the middle of acquiring locks, making it much more likely
   that another thread will jump in and create the deadlock scenario.

## Bonus — AtomicInteger vs Normal int

```java
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 threadCount = 1000;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    normalCounter++;
                    atomicCounter.incrementAndGet();
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println("Expected: " + (threadCount * 1000));
        System.out.println("Normal int result: " + normalCounter);
        System.out.println("AtomicInteger result: " + atomicCounter.get());
    }
}
```

The normal int will show a wrong number because threads interfere with each other.
The AtomicInteger will always show the correct result of 1000000.