1 - What are atomic variables?

Atomic variables are variables that support thread-safe operations without using explicit locks.

They ensure that operations such as incrementing, decrementing, or updating a value are performed as a single indivisible action.

Difference from ordinary variables:

Ordinary variable: Multiple threads can modify it simultaneously, causing race conditions.
Atomic variable: Operations are performed atomically, preventing race conditions for single-variable updates.

2 - Name at least four classes from the java.util.concurrent.atomic package that provide atomic operations for different data types.

AtomicInteger  
AtomicLong  
AtomicBoolean  
AtomicReference<T>

Typical use case for AtomicInteger:     
AtomicInteger is commonly used as a thread-safe counter when multiple threads need to increment a shared value concurrently.

3 - Compare locks with atomic variables.

atomic variables:

Advantages:

Faster than locks for simple operations.  
Non-blocking.  
Lower overhead.  

Best for:

Counters.   
Flags.  
Single-variable updates.    
Locks

locks:

Advantages:     
Can protect multiple variables together.
Support complex critical sections.

Best for:

Operations involving multiple shared objects.
Complex business logic requiring mutual exclusion.

Summary:

Use atomic variables for simple thread-safe updates.
Use locks when multiple operations must be performed as one atomic unit.

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

        int numThreads = 10;
        int incrementsPerThread = 100000;

        Thread[] threads = new Thread[numThreads];

        for (int i = 0; i < numThreads; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < incrementsPerThread; 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());
    }
}

Expected Result:

Normal Counter: 823451   (varies)
Atomic Counter: 1000000

The normal counter may be incorrect because multiple threads overwrite each other's updates. The atomic counter always produces the correct result.

4 - A program is completely free of race conditions but still performs poorly under high contention.
Explain how this situation can occur.

A program can be completely correct yet still perform poorly because threads spend too much time waiting for shared resources.

Three factors that limit scalability:

Lock Contention     
Many threads compete for the same lock and must wait.

Synchronization Overhead    
Acquiring and releasing locks consumes CPU time.

Thread Blocking     
Threads frequently pause while waiting for locks or resources, reducing parallelism.

Correctness is guaranteed, but throughput decreases as contention increases.

5 - Many concurrent systems experience performance degradation as the number of threads increases.
Explain why adding more threads does not always improve performance.

The CPU must save and restore thread state when switching between threads. Too many threads increase this overhead.

Contention      
More threads compete for the same locks and resources, causing waiting.

Cache Coherence     
When multiple CPUs modify shared data, cache contents must be synchronized, creating extra communication overhead.

Synchronization Overhead    
Locks, atomic operations, and coordination mechanisms consume time and reduce the benefits of parallel execution.

Conclusion: After a certain point, adding more threads increases overhead more than useful work, causing performance to stagnate or even decrease.

6 - Deadlocks often only appear in production, not during testing.Explain why this might happen from a thread-scheduling perspective.

Deadlocks depend on the exact timing and scheduling of threads.
During testing, threads may execute in a favorable order and never enter the deadlock state. In production, different workloads, hardware, and timing conditions can cause the problematic scheduling sequence to occur.

Two ways to expose deadlocks during testing

1. Increase concurrency     
Run with many threads.execute tests repeatedly under heavy load.


2. Introduce timing variations   
Add random delays (Thread.sleep()).
Use stress-testing tools to force different thread interleavings.

These techniques increase the chance that threads acquire locks in a problematic order, revealing deadlocks before deployment.