Synchronization — synchronized, volatile, atomic

Thread-এর মধ্যে state নিরাপদে share করা

Read: ~32 min Advanced 5 practice problems Live code runner

1. The Race Condition

When two threads read and write the same variable, things go wrong. counter++ looks like one step but is actually three — read, add, write. If two threads interleave those three steps, one increment is lost. That is a race condition, and it is the root cause of almost every concurrency bug.

দুটি thread একই variable-এ read-write করলে সমস্যা হয়। counter++ দেখতে একটি operation, কিন্তু ভেতরে তিনটি — read, add, write। দুই thread এই তিন step interleave করলে একটি increment হারিয়ে যায়। এটাই race condition — প্রায় সব concurrency bug-এর মূল কারণ।
Main.java
class Main {
    static int counter = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) counter++;
        };
        Thread a = new Thread(task);
        Thread b = new Thread(task);
        a.start(); b.start();
        a.join();  b.join();

        // expected 20000 — usually less (lost updates)
        System.out.println("counter = " + counter);
    }
}

2. synchronized — Mutual Exclusion

The classic fix: put the critical section inside a synchronized block. Exactly one thread can hold a given monitor lock at a time. You can mark a whole method synchronized, or a specific block with a lock object.

classic সমাধান — critical section synchronized block-এ রাখুন। একই monitor lock-এ একসাথে একটি মাত্র thread থাকতে পারে। পুরো method বা একটি block-কে একটি lock object দিয়ে synchronized করা যায়।
Main.java
class Main {
    static int counter = 0;
    static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (LOCK) {
                    counter++;
                }
            }
        };
        Thread a = new Thread(task);
        Thread b = new Thread(task);
        a.start(); b.start();
        a.join();  b.join();

        System.out.println("counter = " + counter);  // always 20000
    }
}
Rule: every field that can be touched by more than one thread needs a clear concurrency policy — usually "all access must happen under lock X". Write the policy in a comment above the field.

3. volatile — Visibility, Not Atomicity

Without synchronization, one thread's writes may never be seen by another — modern CPUs cache values in registers. volatile on a field forces every read/write to go to main memory, giving visibility. But it does not make compound operations atomic. volatile int x; x++; is still a race.

sync ছাড়া এক thread-এর write অন্য thread কখনো দেখতে নাও পারে — CPU register-এ cache করে। volatile বললে প্রতিটি read/write main memory-তে যায় — visibility নিশ্চিত। কিন্তু এটি atomicity দেয় না। volatile int x; x++; এখনো race।
Main.java
class Main {
    static volatile boolean done = false;

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            int spins = 0;
            while (!done) spins++;
            System.out.println("worker saw done; spun " + spins + " times");
        });
        worker.start();
        Thread.sleep(30);
        done = true;  // visible to worker thanks to volatile
        worker.join();
    }
}

4. Atomics — Lock-Free Counters

java.util.concurrent.atomic provides types like AtomicInteger, AtomicLong, AtomicReference. Their incrementAndGet, compareAndSet, etc. are atomic at the hardware level (compare-and-swap). They are fast and perfect for counters.

AtomicInteger, AtomicLong, AtomicReference — lock ছাড়াই atomic। CPU-র compare-and-swap (CAS) instruction ব্যবহার করে। counter-এর জন্য সবচেয়ে দ্রুত সমাধান।
Main.java
import java.util.concurrent.atomic.*;

class Main {
    static final AtomicInteger counter = new AtomicInteger();

    public static void main(String[] args) throws Exception {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) counter.incrementAndGet();
        };
        Thread a = new Thread(task), b = new Thread(task);
        a.start(); b.start();
        a.join();  b.join();
        System.out.println("counter = " + counter.get());  // 20000
    }
}

5. ReentrantLock — Explicit Locking

ReentrantLock is like synchronized but with extra features: timed acquisition, interruptible waits, and fairness. Use it only when you actually need those features — otherwise synchronized is simpler and just as fast.

ReentrantLock = synchronized + extra feature (timed acquire, interruptible wait, fairness)। এই feature দরকার হলেই ব্যবহার করুন, নাহলে synchronized-ই simple ও same-fast।
Main.java
import java.util.concurrent.locks.*;

class Main {
    static final ReentrantLock lock = new ReentrantLock();
    static int balance = 100;

    static void withdraw(int amt) {
        lock.lock();
        try {
            if (balance >= amt) balance -= amt;
        } finally {
            lock.unlock();  // always release
        }
    }

    public static void main(String[] args) throws Exception {
        Runnable r = () -> { for (int i = 0; i < 20; i++) withdraw(1); };
        Thread a = new Thread(r), b = new Thread(r);
        a.start(); b.start();
        a.join();  b.join();
        System.out.println("balance = " + balance);
    }
}

6. Deadlock — and How to Avoid It

Two threads hold a lock each and wait for the other's lock — nothing moves forever. The fix is simple: always acquire locks in the same global order. If every thread locks A before B, deadlock is impossible.

দুই thread একটি করে lock ধরে আছে এবং একে অপরের lock-এর জন্য অপেক্ষা করছে — চিরকাল। সমাধান — সবসময় একই global order-এ lock নিন। সব thread আগে A পরে B lock করলে deadlock অসম্ভব।
Deadlock — each thread waits for the other's lock Thread 1 holds A, wants B Thread 2 holds B, wants A Lock A Lock B Fix: always lock A before B (same global order) Figure 38.1 — lock always in a consistent order।

7. Vocabulary

TermMeaningবাংলায়
Race conditionOutcome depends on thread scheduling — a bug.thread scheduling-এর উপর ফলাফল নির্ভর — bug।
Critical sectionCode that must run without interference.বিনা হস্তক্ষেপে চলতে হওয়া code।
synchronizedKeyword that gives mutual exclusion via a monitor lock.monitor lock দিয়ে mutual exclusion।
volatileField modifier guaranteeing visibility, not atomicity.visibility দেয়, atomicity নয়।
CASCompare-And-Swap — hardware atomic instruction.hardware-level atomic instruction।
DeadlockTwo+ threads stuck waiting on each other.একে অপরের lock-এর জন্য চিরকাল অপেক্ষা।
ReentrantA thread can re-acquire a lock it already holds.নিজের ধরা lock আবার ধরতে পারে।

8. Practice Problems

  1. Fix a lost-update counter with synchronized.
    lost-update-এ আক্রান্ত counter-কে synchronized দিয়ে fix করুন।
    Show Answer (উত্তর দেখুন)
    Main.java
    class Main {
        static int c = 0;
        static final Object L = new Object();
        public static void main(String[] args) throws Exception {
            Runnable r = () -> {
                for (int i = 0; i < 5000; i++) synchronized (L) { c++; }
            };
            Thread a = new Thread(r), b = new Thread(r);
            a.start(); b.start(); a.join(); b.join();
            System.out.println(c);
        }
    }
  2. Explain in 2 sentences why volatile alone cannot make x++ safe.
    দুই বাক্যে বলুন — volatile একা x++-কে নিরাপদ করতে পারে না কেন।
    Show Answer (উত্তর দেখুন)

    Answer: volatile guarantees that reads and writes cross the memory boundary correctly, but x++ is three separate operations — read, add, write — and two threads can still interleave those steps. Only an atomic primitive (AtomicInteger) or a lock closes that window.

    volatile শুধু read/write main memory-তে যাওয়া guarantee দেয়, কিন্তু x++ তিনটি step (read, add, write) — দুই thread interleave করলেই loss। AtomicInteger বা lock-ই সমাধান।

  3. Use AtomicInteger to count how many times two threads together called a method.
    AtomicInteger দিয়ে গুনুন — দুটি thread একসাথে method-টি কতবার call করেছে।
    Show Answer (উত্তর দেখুন)
    Main.java
    import java.util.concurrent.atomic.*;
    class Main {
        static final AtomicInteger calls = new AtomicInteger();
        static void hello() { calls.incrementAndGet(); }
        public static void main(String[] args) throws Exception {
            Runnable r = () -> { for (int i = 0; i < 1000; i++) hello(); };
            Thread a = new Thread(r), b = new Thread(r);
            a.start(); b.start(); a.join(); b.join();
            System.out.println(calls.get());
        }
    }
  4. Write a thread-safe Counter class with increment and get.
    একটি thread-safe Counter class লিখুন — increment ও get।
    Show Answer (উত্তর দেখুন)
    Main.java
    class Main {
        static class Counter {
            private int n = 0;
            synchronized void increment() { n++; }
            synchronized int get() { return n; }
        }
        public static void main(String[] args) throws Exception {
            Counter c = new Counter();
            Runnable r = () -> { for (int i = 0; i < 1000; i++) c.increment(); };
            Thread a = new Thread(r), b = new Thread(r);
            a.start(); b.start(); a.join(); b.join();
            System.out.println(c.get());
        }
    }
  5. In 3 sentences, describe the golden rule that prevents deadlock.
    তিন বাক্যে deadlock এড়ানোর সোনালী নিয়ম বলুন।
    Show Answer (উত্তর দেখুন)

    Answer: Always acquire multiple locks in the same total order across all threads — define a well-known ranking and never take lock B while holding lock A if A comes after B in that ranking. If you cannot enforce an order (dynamic objects), use tryLock with a timeout and back off when you fail. Finally, keep critical sections short — fewer chances to collide, less damage if you do.

    সব thread-এ একই global order-এ lock নিন। dynamic object হলে tryLock + timeout, ব্যর্থ হলে back off। critical section ছোট রাখুন — কম collision, কম ক্ষতি।

Summary — Module 38

Shared mutable state is concurrency's worst enemy. synchronized gives mutual exclusion, volatile gives visibility but not atomicity, atomics give lock-free updates for simple fields, and ReentrantLock gives you advanced control when you need it. Write down each field's concurrency policy, lock in a consistent global order, and keep critical sections short.

shared mutable state concurrency-র প্রধান শত্রু। synchronized = mutual exclusion, volatile = visibility (atomicity নয়), atomics = lock-free, ReentrantLock = advanced control। প্রতিটি field-এর policy লিখুন, consistent order-এ lock নিন, critical section ছোট রাখুন।

Next Module → Executor Framework ও CompletableFuture।