Files
2026-06-15 01:27:43 +04:30

6.0 KiB

⚛️ Atomic Variables & Synchronization


1 - Atomic variables solve thread-safety problems without using explicit synchronization (synchronized blocks or Lock objects). Their primary purposes are:

  1. Provide thread-safe operations on single variables without locks

  2. Enable non-blocking algorithms with better performance under moderate contention

  3. Ensure visibility across threads (like volatile variables)

  4. Support atomic read-modify-write operations (e.g., increment, compare-and-set)

2 - Four Atomic Classes from java.util.concurrent.atomic:

  1. AtomicInteger: Atomic operations for int values

  2. AtomicLong: Atomic operations for long values

  3. AtomicBoolean: Atomic operations for boolean values

  4. AtomicReference: Atomic operations for object references

  • A use case is when a web server needs to generate unique, sequential request IDs across thousands of concurrent threads.

3 - When to use…

  • Atomic Variables: when you're updating only one variable with simple operations under moderate contention.

  • Locks: when you need to coordinate multiple resources, have complex conditions, or require fairness or blocking semantics.

Aspect Locks (synchronized, ReentrantLock) Atomic Variables
Mechanism Blocking (threads wait/park) Non-blocking (CAS operations)
Scope Can protect multiple variables/operations Single variable only
Overhead Higher (context switching, OS involvement) Lower (CPU-level instructions)
Contention handling Threads block and may be descheduled Threads retry without blocking
Deadlock risk Yes (especially with multiple locks) No
Fairness options Available (e.g., ReentrantLock(true)) No (CAS is inherently unfair)
Composite operations Easy (multiple steps together) Difficult or impossible

- Bonus:

import java.util.concurrent.atomic.AtomicInteger;

public class example {
    private static int count1;
    private static AtomicInteger count2 = new AtomicInteger(0);

    public example() {}

    public static void increment() { count1++; }

    public static void atomicIncrement() { count2.incrementAndGet(); }

    public static void main(String args[]) throws InterruptedException {

        Runnable r = () -> {
            for (int i = 1; i <= 1000; i++) {
                atomicIncrement();
                increment();
            }
        };
        
        Thread t1 = new Thread(r, "thread-1");
        Thread t2 = new Thread(r, "thread-2");
        Thread t3 = new Thread(r, "thread-3");

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("-int- variable value: " + count1 + " -AtomicInteger- variable value: " + count2);
    }
}

Output:

-int- variable value: 2971 -AtomicInteger- variable value: 3000

🔒 Locks & Concurrent Design


4 - A program can be free of race conditions through proper synchronization (locks, atomic variables, concurrent collections), but still suffer from:

  • Contention: Threads competing for the same resources

  • Over-synchronization: Too much or too coarse-grained locking

  • Hardware limitations: Cache coherence traffic, false sharing

Concurrency-related factors that may limit scalability even when correctness is guaranteed:

  1. Lock Contention (Serialization): Even with correct locking, if threads frequently wait for locks, execution becomes effectively serialized. Threads queue up despite having multiple CPU cores.

  2. Cache Coherence Traffic (The "Hidden" Contention): Even lock-free atomic variables generate significant CPU cache traffic that limits scalability.

  3. False Sharing: When threads modify different variables that accidentally share the same CPU cache line, the cache coherence protocol treats them as if they were the same variable.

5 - This is the scalability ceiling problem. Adding threads eventually hurts performance due to coordination overhead.

  1. Context Switching Overhead: When more threads than CPU cores exist, the OS constantly switches between threads, saving/restoring state.

  2. Contention for Shared Resources: As threads increase, probability of conflict superlinearly increases.

  3. Cache Coherence Traffic: Modern CPUs maintain cache coherence (MESI protocol). When multiple cores modify shared data, they generate coherence traffic that doesn't exist with fewer threads.

  4. Synchronization Overhead (Locking): Even without contention, acquiring/releasing locks has intrinsic overhead.

⚠️ Deadlocks


6 - A deadlock requires four conditions:

  • Mutual exclusion: Resources can't be shared

  • Hold and wait: Thread holds one resource while waiting for another

  • No preemption: Resources can't be forcibly taken

  • Circular wait: Threads form a cycle of dependencies

But even with all conditions present, a deadlock only occurs when threads acquire locks in precisely the wrong order at precisely the wrong time. Specific scheduling factors that hide deadlocks:

  1. Low contention in tests: Threads rarely overlap in lock acquisition

  2. Fast operations: Critical sections so brief that interleaving is improbable

  3. Small thread pools: Fewer permutations of lock ordering

  4. Deterministic scheduling: Same interleaving every run (lulls developers into false safety)

  5. No GC pressure: GC can pause threads at vulnerable moments

Strategy 1 : Manual Lock Order Inversion (Eclipse Contest) Force the circular wait condition by deliberately inverting lock order in tests.

Strategy 2 : Inject Preemptive Scheduling Points (ThreadWeaver) Force thread context switches at vulnerable points using controlled test frameworks like ThreadWeaver or JCStress.