HW09-Theoretical Questions

1. What are atomic variables?

Atomic variables are variables that can be safely used in multithreaded programs without using locks or synchronized blocks. Their operations are performed atomically, which means that a thread can complete an operation without interference from other threads.
The main purpose of atomic variables is to prevent race conditions. Unlike normal variables, atomic variables guarantee that updates are done safely when several threads access the same variable at the same time.


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

Examples of atomic classes are:
"AtomicInteger"
"AtomicLong"
"AtomicBoolean"
"AtomicReference"

A common use of "AtomicInteger" is implementing a shared counter. Multiple threads can increment the counter safely without using locks.


3. Compare locks with atomic variables.

Atomic variables are usually faster because they do not block threads. They are suitable for simple operations on a single variable, such as increasing a counter.

Locks are more suitable when several operations or multiple shared variables must be protected together. Although locks have more overhead, they provide greater flexibility.

In short, atomic variables are better for simple tasks, while locks are preferred for more complex critical sections.


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

A program may be correct but still have poor performance when many threads are running simultaneously.

Some factors that can reduce performance are:
1. Lock contention: many threads may wait for the same lock.
2. Synchronization overhead: locking and unlocking repeatedly adds extra cost.
3. Context switching: the operating system spends time switching between threads instead of executing them.

Therefore, correctness does not always mean good scalability.

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

Adding more threads is not always beneficial because threads also create overhead.

- Context switching: frequent switching between threads consumes CPU time.
- Contention: threads may compete for shared resources.
- Cache coherence: changes made by one core may force other cores to update their caches.
- Synchronization overhead: synchronization mechanisms themselves require additional processing.

As a result, after a certain point, increasing the number of threads may even decrease performance.

6. Why do deadlocks often appear only in production?

Deadlocks depend on thread scheduling, which is unpredictable. During testing, the program may run without problems because threads happen to execute in a safe order. In production, different workloads and execution timings can expose deadlocks.

Two ways to increase the chance of finding deadlocks during testing are:

1. Running stress tests with many threads and many operations.
2. Running tests repeatedly and adding random delays to create different execution orders.


Bonus Task

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 Exception {

        Thread[] threads = new Thread[100];

        for (int i = 0; i < threads.length; i++) {

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

            threads[i].start();
        }

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

        System.out.println("Normal Counter: " + normalCounter);
        System.out.println("Atomic Counter: " + atomicCounter.get());
    }
}