⚡IntermediateConcurrency Basics

Synchronization

Use synchronized, volatile, and atomic classes to safely share data between threads.

Overview

When multiple threads access shared mutable data without coordination, you get race conditions: unpredictable bugs caused by concurrent reads and writes. Synchronization prevents this. Java provides several tools: 1. synchronized keyword: locks a monitor object so only one thread at a time can execute the synchronized block or method. 2. volatile: ensures that a field is read from and written to main memory, not a thread-local cache. 3. Atomic classes (AtomicInteger, AtomicLong, etc.): thread-safe wrappers around single values, using CPU-level compare-and-swap instructions instead of locks. Each tool has a different cost and use case. Prefer the highest-level tool that meets your needs.

Key Points

  • •synchronized(obj) { ... } acquires the monitor of obj before entering
  • •synchronized methods use this (or the class) as the monitor
  • •Only one thread can hold a monitor at a time; others wait
  • •volatile guarantees visibility but not atomicity. Suitable for single-variable flags, not compound operations
  • •AtomicInteger: compareAndSet, incrementAndGet, addAndGet are atomic
  • •Race condition: two threads read, modify, and write the same variable non-atomically
  • •Deadlock: two threads each wait for a lock the other holds. Avoid by always acquiring locks in the same order

Examples

Race condition and synchronized fix

public class Main {
    static int counter = 0;

    static synchronized void increment() {
        counter++; // now atomic: only one thread at a time
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();

        System.out.println("Final counter: " + counter);
        // Without synchronized: likely less than 100000
        // With synchronized: always 100000
    }
}

Without synchronized, counter++ is three non-atomic steps (read, add, write). Two threads can interleave and one increment is lost. synchronized ensures the whole method is atomic.

AtomicInteger for lock-free counting

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);

        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.incrementAndGet(); // atomic, no lock needed
                }
            });
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();

        System.out.println("Final counter: " + counter.get()); // always 100000
        System.out.println("Compare-and-set: " + counter.compareAndSet(100000, 0));
        System.out.println("After reset: " + counter.get());
    }
}

AtomicInteger is faster than synchronized for single-value counters because it uses CPU-level compare-and-swap (CAS) instructions instead of a kernel-level lock.

volatile for visibility and synchronized block

public class Main {
    static volatile boolean running = true; // volatile: changes visible across threads

    static class Counter {
        private int count = 0;

        synchronized void increment() { count++; }
        synchronized int get() { return count; }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread worker = new Thread(() -> {
            while (running) { // reads running from main memory due to volatile
                counter.increment();
            }
            System.out.println("Worker stopped. Count: " + counter.get());
        });

        worker.start();
        Thread.sleep(10); // let worker run a bit
        running = false;  // this write is immediately visible to worker due to volatile
        worker.join();
    }
}

volatile ensures the worker sees the updated value of running immediately. Without volatile, the JVM can cache running in the worker thread and never see the update from main.

Common Mistakes

  • Using synchronized on different objects for the same data. If method A synchronizes on this and method B synchronizes on a different object, they do not protect the same critical section
  • Over-synchronizing: holding a lock longer than necessary hurts performance. Use the smallest possible synchronized block
  • Using volatile for compound operations like counter++. volatile does not make the read-modify-write sequence atomic. Use AtomicInteger or synchronized for compound operations

Note

For simple counters and flags, prefer AtomicInteger/AtomicBoolean over synchronized. For protecting larger state or multiple variables, use synchronized. For one-time flags, consider volatile.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What problem does synchronized solve?