Synchronization and race conditions

Threads share the heap, and that's both their power and their danger. When two threads change the same data at the same time, updates get lost and balances go negative. You'll see a race condition happen in real code, understand exactly why at the bytecode level, and fix it with synchronized, atomic classes and volatile, while avoiding the classic trap: deadlock.

Module 12 · Multithreading⏱ 25 minIntermediate9 practice problems
1 The story

The likes counter that couldn't count

Riya's canteen app has a “like” button for dishes. In testing, two phones tap Like 100,000 times each. The counter should say 200,000. It says 148,870. Run again: 169,997.

likes 148,870 Phone A100,000 taps Phone B100,000 taps expected 200,000: about 51,000 likes lost (numbers from a real run; yours will differ) myphatshala.in
Riya

Sir, the code is just likes++. How can Java lose a plus-one?

Arjun Sir

likes++ is three steps: read, add, write. If both threads read the same old value, one write overwrites the other. That's a race condition.

Riya

So threads sharing data is dangerous?

Arjun Sir

Unprotected sharing is. The fix is to make the read-add-write happen as one indivisible step, one thread at a time. That's what synchronized does.

2 Definition

What are race conditions and synchronization?

Formal definition (write this in exams)

A race condition occurs when the result of a program depends on the timing or interleaving of threads that access shared mutable data, at least one of them writing. The code that accesses such data is a critical section. Synchronization ensures mutual exclusion (only one thread executes a critical section at a time) and visibility (changes made by one thread are seen by others). In Java, every object has an intrinsic lock (monitor); a synchronized method or block acquires that lock on entry and releases it on exit, and a thread that cannot acquire it is BLOCKED.

In simple words

Shared data is a single-person changing room. synchronized is the lock on the door: one person inside at a time, the rest wait in line.

Words you'll meet in this lesson

Race conditionWrong results that depend on which thread runs when.
Critical sectionCode that reads or writes shared data and must not interleave.
Monitor / intrinsic lockThe lock every Java object has; one owner at a time.
AtomicHappens as one indivisible step: no other thread sees it half-done.
VisibilityWhether one thread's write is seen by another thread.
DeadlockThreads waiting for each other's locks forever.

Your three tools

synchronizedAtomic classesvolatile
Mutual exclusionYesFor one variable's updateNo
VisibilityYesYesYes
Makes x++ safeYesYes (incrementAndGet)No
Protects several fields togetherYesNoNo
Can block threadsYesNo (lock-free)No
Typical useBalance check + debitCounters, IDsStop flags, status

Exam tip: “volatile guarantees visibility but not atomicity.” A volatile int with count++ still loses updates. Also: a synchronized instance method locks this; a static synchronized method locks ClassName.class.

3 Types and ways to use it

See the race, then fix it

The race

public class Main {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                count++;
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println("expected 200000, got " + count);
    }
}
expected 200000, got 128965

This output is from one real run. Yours will be different, and different each time; that's what makes race conditions so hard to catch in testing.

The fix: a synchronized method

public class Main {
    static int count = 0;

    static synchronized void increment() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                increment();
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println("expected 200000, got " + count);
    }
}
expected 200000, got 200000

The ways to synchronize

synchronized method

The whole method is the critical section. Locks this (or the class, if static).

simplest

synchronized block

Lock only the lines that touch shared data, on a lock object you choose.

fine-grained

Atomic classes

AtomicInteger, AtomicLong, LongAdder: safe single-variable updates without locks.

counters

volatile

Every thread always sees the latest value. For flags one thread writes and others read.

visibility
synchronized void deposit(int amt) { … }        // locks this
static synchronized void log(String s) { … }    // locks the class object (Bank.class)
synchronized (lock) { … }                       // locks the object you choose

volatile boolean running;                       // visibility only, no locking
AtomicInteger hits = new AtomicInteger();       // lock-free atomic updates
hits.incrementAndGet();

The trap: deadlock

// Thread 1                       // Thread 2
synchronized (accountA) {          synchronized (accountB) {
    synchronized (accountB) {          synchronized (accountA) {   // each waits for the
        …                                  …                       // other's lock: forever
    }                                  }
}                                  }

Always take multiple locks in the same order. If every thread locks account A before account B (for example, by smaller account id first), the circular wait above can never happen. And keep locked sections short: never call a slow network service while holding a lock.

4 Step by step

What happens at a synchronized block

  1. A thread reaches the block

    synchronized (account) {
        if (account.balance >= amt) account.balance -= amt;
    }
    The JVM runs monitorenter on the account object's monitor.
  2. Lock free? Take it

    If no one owns the monitor, this thread becomes the owner and enters. It also sees every write made by the previous owner before it released the lock.
  3. Lock taken? Wait

    If another thread owns it, this thread joins the monitor's entry set and is BLOCKED. It uses no CPU while waiting.
  4. Leave the block

    monitorexit releases the lock, even if an exception is thrown inside (the compiler adds a hidden finally). All writes made inside become visible to the next owner.
  5. Next in line

    One BLOCKED thread acquires the lock and becomes RUNNABLE. Which one is not guaranteed. The same thread may re-enter a lock it already owns (locks are re-entrant).
5 See it happen

A lost update, and the lock that prevents it

Left: two threads and one shared count. Right: the bytecode steps of count++, each thread's private operand stack, and the monitor.

A race condition and its fix 6 steps
What you see count 5 5 5 6 6 7 T1 T2 count++ count++ read 5 read 5 write 6 write 6 2 increments, count went up by 1 synchronized has lock: 5→6 BLOCKED done has lock: 6→7 they take turns: no update lost Two threads, one shared count. T1 reads 5… …T2 also reads 5. Both write 6: one update lost. With synchronized, T2 must wait. T2 then sees 6 and writes 7. myphatshala.in
Behind the screen count++ in bytecode getstatic count iconst_1 iadd putstatic count T1 operand stack T2 operand stack 5 6 5 6 a switch can happen between any two of these steps monitor of the lock object owner: T1 owner: T2 entry set: T2 (BLOCKED) entry set: (empty) monitorenter … monitorexit count on the heap: 5 5 5 6 6 7 One line of Java… …is four bytecode steps. T2 interleaves after T1's read. Two writes of 6. The lock has one owner at a time. Ownership passes to T2. myphatshala.in
6 Real-world use

Where races cost real money

Race conditions are behind overselling in flash sales, double payouts, duplicate order numbers and corrupted caches.

Watch it with an example: two ATMs, one joint account

A joint account has ₹1,000. Two holders withdraw ₹800 at two ATMs at the same moment. Without a lock, both succeed.

Two ATMs, one balance 6 steps
Animated example balance ₹1000 ₹1000 ₹200 ₹-600 ₹200 ₹200 ATM 1 ATM 2 1000 ≥ 800 ✓ 1000 ≥ 800 ✓ pay ₹800 pay ₹800 overdrawn by ₹600! synchronized withdraw 1000 ≥ 800 ✓ paid ₹800 waits… 200 ≥ 800 ✗ refused balance never goes negative ₹1000 in the account. Both ATMs check the balance. ATM 1 pays out. ATM 2 pays out too. With a lock: ATM 2 waits. Then it sees ₹200 and refuses. myphatshala.in
₹

1. Balances and wallets

Debit must check and update the balance as one step, or two parallel payments can overdraw it.

2 payments at once→synchronized debit()→never negative

2. Inventory in flash sales

Thousands of buyers hit “Buy” for 100 units. Reservation must be atomic so exactly 100 orders succeed.

10,000 clicks→atomic reserve→100 orders
#1042

3. IDs and counters

Order numbers, page views and rate-limit counters use AtomicLong or LongAdder so no two orders get the same number.

new order→ids.incrementAndGet()→unique id

4. Shared caches and config

Concurrent maps (ConcurrentHashMap) and volatile references let many request threads read config and cached data safely while one thread refreshes it.

refresh thread→volatile config→all threads see it

Simplified: real banking systems also rely on database transactions and row locks, because many servers (not just many threads) share the data.

7 Run the code

The same race, fixed with a block

public class Main {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                synchronized (Main.class) {
                    count++;
                }
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println("synchronized result: " + count);
    }
}
synchronized result: 200000
  • synchronized (Main.class)Both threads lock the same object (the Class object of Main), so only one is inside at a time.
  • count++Now the read-add-write can't interleave with the other thread's.
  • a.join(); b.join();join also guarantees main sees both threads' final writes.
8 Line by line

A synchronized counter shared by two threads

Recorded from the real JVM, following main. Watch the single Counter object on the heap: both threads update it, and the result is right every time.

Worked examples: real problems, solved and traced in memory

Three ways to make shared data safe: a synchronized method, an atomic class, and a volatile flag. Each trace follows main; the other threads' work shows up in the shared objects on the heap.

Example 1Ticketing · last seat

Only one person gets the last seat

Problem statement

A concert has 1 seat left. Two customers press Book at the same moment, each on their own thread. Exactly one booking must succeed. Make book() safe and print how many bookings succeeded and how many seats are left.

Input
seats left: 1; two booking threads
Expected output
successful bookings: 1, seats left: 0

Approach

  1. The check (left > 0) and the update (left--) must happen as one step, so put both inside a synchronized method.
  2. Each thread stores whether it succeeded in its own array slot.
  3. After joining, count the successes. The winner's name may vary; the counts never do.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Payments · cashback

Cashback counter with AtomicInteger

Problem statement

Four payment servers each credit ₹1 cashback 1,000 times to the same wallet total. Use AtomicInteger so no credit is lost, without writing synchronized.

Input
4 threads × 1,000 credits of ₹1
Expected output
total cashback: Rs 4000

Approach

  1. AtomicInteger.incrementAndGet() performs read-add-write as one indivisible hardware operation (compare-and-swap).
  2. All four threads share one AtomicInteger.
  3. Join all, then print the total: always 4000.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Streaming · shutdown

Stop a worker with a volatile flag

Problem statement

A video-encoding worker keeps processing chunks while a flag running is true. main lets it work for a moment, then sets the flag to false. Without volatile, the worker might never see the change. Make the stop reliable.

Input
(no input)
Expected output
encoder: saw running = false, stopping
encoded some chunks: true

Approach

  1. Declare static volatile boolean running = true;.
  2. The worker loops while (running).
  3. main sleeps briefly, sets running = false, joins the worker, and reports.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 12.4 in one screen

  • Race condition: result depends on thread timing; needs shared mutable data and at least one writer.
  • x++ is read-add-write, not atomic. Check-then-act (if (bal >= amt) bal -= amt) is also a race.
  • synchronized = mutual exclusion + visibility, using an object's monitor. Instance method → this; static → Class.
  • Threads must lock the same object to exclude each other.
  • volatile = visibility only. Atomic classes = lock-free safe updates of one variable.
  • Deadlock: circular wait for locks. Prevent with a fixed lock order and short critical sections.
  • Locks are re-entrant and released even when an exception is thrown.

Interview and viva questions

What is a race condition? Give an example.

A bug where the outcome depends on the interleaving of threads accessing shared mutable data. Example: two threads run count++ on a shared int; both read the same value and one increment is lost. Another: two withdrawals both pass a balance check before either debits.

Difference between a synchronized method and a synchronized block?

A synchronized method locks the whole method on this (or the Class for static methods). A block locks only its statements, on any object you choose. Blocks keep critical sections smaller and let you use private lock objects that outside code can't interfere with.

What is the difference between volatile and synchronized?

volatile ensures reads and writes of one variable go to main memory and aren't reordered, giving visibility. It doesn't make compound actions atomic. synchronized provides both mutual exclusion and visibility for any block of code, at the cost of possible blocking.

What is a deadlock and how do you prevent it?

Two or more threads each hold a lock and wait for a lock held by another, forever. Prevent it by acquiring locks in a consistent global order, holding locks briefly, avoiding calling unknown code while holding a lock, or using ReentrantLock.tryLock with a timeout.

How does AtomicInteger work without locks?

It uses a CPU compare-and-swap (CAS) instruction: read the value, compute the new one, and write it only if the value is still what was read; otherwise retry. No thread blocks, so there's no deadlock risk, though heavy contention can cause retries (LongAdder reduces that).

Practice problems

Find the shared data, then protect it with synchronized, an atomic class, or volatile. Every solution prints the same result on every run. The last three are business tasks.

P1Easy

Synchronized counter

Two threads each call inc() 50,000 times on one counter. Make the total always 100000.

Input
2 × 50,000
Expected output
100000
Show solution and explanation
class Counter {
    private int n;
    synchronized void inc() { n++; }
    synchronized int get() { return n; }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        Runnable job = () -> { for (int i = 0; i < 50_000; i++) c.inc(); };
        Thread a = new Thread(job), b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(c.get());
    }
}
100000

How it works: a synchronized instance method locks this. Both threads use the same Counter, so they take turns inside inc().

P2Easy

Synchronized block on a lock object

Do the same with a static int and a synchronized (LOCK) block around count++ only.

Input
2 × 50,000
Expected output
100000
Show solution and explanation
public class Main {
    static int count = 0;
    static final Object LOCK = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 50_000; i++) {
                synchronized (LOCK) {
                    count++;
                }
            }
        };
        Thread a = new Thread(job), b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(count);
    }
}
100000

How it works: a block lets you lock only the lines that touch shared data. Make the lock object final so it can never be replaced.

P3Easy

AtomicInteger

Three threads each add 10 to an AtomicInteger 100 times using addAndGet. Print the result.

Input
3 × 100 × 10
Expected output
3000
Show solution and explanation
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger total = new AtomicInteger();
        Thread[] ts = new Thread[3];
        for (int i = 0; i < 3; i++) {
            ts[i] = new Thread(() -> { for (int k = 0; k < 100; k++) total.addAndGet(10); });
            ts[i].start();
        }
        for (Thread t : ts) t.join();
        System.out.println(total.get());
    }
}
3000

How it works: atomic classes use a CPU compare-and-swap instruction, retrying if another thread got there first. No thread is ever BLOCKED.

P4Medium

Static synchronized across objects

Each thread uses its own Visit object, but all visits must be counted in one static total. Use a static synchronized method.

Input
4 threads × 2,500 visits
Expected output
visits: 10000
Show solution and explanation
class Visit {
    private static int total = 0;

    static synchronized void record() { total++; }
    static synchronized int total() { return total; }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread[] ts = new Thread[4];
        for (int i = 0; i < 4; i++) {
            ts[i] = new Thread(() -> { for (int k = 0; k < 2500; k++) Visit.record(); });
            ts[i].start();
        }
        for (Thread t : ts) t.join();
        System.out.println("visits: " + Visit.total());
    }
}
visits: 10000

How it works: a static synchronized method locks the class object Visit.class, one lock for the whole class, which is right for a static field. An instance lock (this) would not protect it if the threads used different objects.

P5Medium

Thread-safe list

Two threads each add 1,000 items to a shared list. A plain ArrayList can lose items or crash. Use Collections.synchronizedList and print the size.

Input
2 × 1,000 items
Expected output
orders: 2000
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        List<Integer> orders = Collections.synchronizedList(new ArrayList<>());
        Runnable job = () -> { for (int i = 0; i < 1000; i++) orders.add(i); };
        Thread a = new Thread(job), b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println("orders: " + orders.size());
    }
}
orders: 2000

How it works: the wrapper makes each method synchronized. ArrayList's add does several steps (check capacity, store, size++), so unsynchronized adds can be lost. For heavy use, prefer java.util.concurrent collections such as CopyOnWriteArrayList or ConcurrentHashMap.

P6Medium

Check-then-act fixed

A coupon can be used at most 3 times. Ten threads try to redeem it. Make redeem() safe and print how many succeeded.

Input
limit 3, 10 attempts
Expected output
redeemed 3 times
Show solution and explanation
class Coupon {
    private int uses = 0;

    synchronized boolean redeem() {
        if (uses < 3) {
            uses++;
            return true;
        }
        return false;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Coupon c = new Coupon();
        boolean[] ok = new boolean[10];
        Thread[] ts = new Thread[10];
        for (int i = 0; i < 10; i++) {
            int k = i;
            ts[i] = new Thread(() -> ok[k] = c.redeem());
            ts[i].start();
        }
        int wins = 0;
        for (int i = 0; i < 10; i++) {
            ts[i].join();
            if (ok[i]) wins++;
        }
        System.out.println("redeemed " + wins + " times");
    }
}
redeemed 3 times

How it works: the comparison and the increment must be in the same synchronized method. Synchronizing them separately would still allow two threads to pass the check before either increments.

P7Business problem

Flash-sale stock reservation

A phone has 5 units in stock. Eight shoppers' threads each try to reserve one. Print reserved and sold-out counts and the final stock.

Input
stock 5, 8 shoppers
Expected output
reserved: 5, sold out for: 3, stock now: 0
Show solution and explanation
class Stock {
    private int units = 5;

    synchronized boolean reserve() {
        if (units == 0) return false;
        units--;
        return true;
    }

    synchronized int units() { return units; }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Stock phone = new Stock();
        boolean[] got = new boolean[8];
        Thread[] shoppers = new Thread[8];
        for (int i = 0; i < 8; i++) {
            int k = i;
            shoppers[i] = new Thread(() -> got[k] = phone.reserve(), "shopper-" + i);
            shoppers[i].start();
        }
        int reserved = 0;
        for (int i = 0; i < 8; i++) {
            shoppers[i].join();
            if (got[i]) reserved++;
        }
        System.out.println("reserved: " + reserved + ", sold out for: " + (8 - reserved) + ", stock now: " + phone.units());
    }
}
reserved: 5, sold out for: 3, stock now: 0

How it works: this is the classic overselling bug in flash sales. Real systems use the same idea one level down: a database row lock or an atomic UPDATE … WHERE units > 0.

P8Business problem

Transfers without deadlock

Two threads run transfers at once: ten of A→B ₹100 and ten of B→A ₹50. Each must lock both accounts. Always lock the account with the smaller id first, so the threads can't deadlock. Print the final balances.

Input
A (id 1) ₹500, B (id 2) ₹300
Expected output
A: 0, B: 800, total: 800
Show solution and explanation
class Account {
    final int id;
    int balance;

    Account(int id, int balance) {
        this.id = id;
        this.balance = balance;
    }
}

public class Main {
    static void transfer(Account from, Account to, int amt) {
        Account first = from.id < to.id ? from : to;
        Account second = first == from ? to : from;
        synchronized (first) {
            synchronized (second) {
                from.balance -= amt;
                to.balance += amt;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1, 500), b = new Account(2, 300);
        Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) transfer(a, b, 100); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) transfer(b, a, 50); });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("A: " + a.balance + ", B: " + b.balance + ", total: " + (a.balance + b.balance));
    }
}
A: 0, B: 800, total: 800

How it works: if t1 locked A then waited for B while t2 locked B then waited for A, both would wait forever: a deadlock. A fixed global lock order makes that cycle impossible. The total stays ₹800. (No balance checks here, so balances may go negative; it's only a locking demo.)

P9Business problem

Page-view counter for a news site

Six web threads each record 5,000 page views. Use LongAdder, which is faster than AtomicLong when many threads update often, and print the total.

Input
6 × 5,000
Expected output
page views: 30000
Show solution and explanation
import java.util.concurrent.atomic.LongAdder;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        LongAdder views = new LongAdder();
        Thread[] web = new Thread[6];
        for (int i = 0; i < 6; i++) {
            web[i] = new Thread(() -> { for (int k = 0; k < 5000; k++) views.increment(); });
            web[i].start();
        }
        for (Thread t : web) t.join();
        System.out.println("page views: " + views.sum());
    }
}
page views: 30000

How it works: LongAdder keeps several internal cells so threads rarely compete for the same one, and sum() adds them up. It's the usual choice for high-traffic counters and metrics.

Real-world scenario questions (asked in company interviews)

Concurrency bugs are favourite interview topics because they are expensive in production and invisible in simple tests.

E-commerce · oversellingFresher

103 orders for 100 phones

During a flash sale, 100 units were listed but 103 orders were confirmed. The code does if (stock > 0) { createOrder(); stock--; } in a request handler. Explain and fix.

The interviewer is checking: check-then-act races.

Show a strong answer
  • Several request threads passed the stock > 0 check before any of them decremented: a check-then-act race.
  • Make the check and decrement one atomic step: a synchronized method, an AtomicInteger with compareAndSet/getAndUpdate, or (across servers) a conditional database update UPDATE … SET stock = stock - 1 WHERE stock > 0.
  • Create the order only after the reservation succeeds.

Avoid: “add a small sleep” or “it only happens under load”. That's exactly when it matters.

Code review · locksJunior

Synchronized, yet still wrong

A colleague wrote synchronized (new Object()) { total += amount; } and says the method is now thread-safe. Tests still show wrong totals. Why?

The interviewer is checking: that exclusion requires a shared lock.

Show a strong answer
  • Every call locks a brand-new object, so no two threads ever compete for the same lock: no exclusion at all.
  • Use one shared, final lock object (private final Object lock = new Object();) or synchronized (this), or an atomic type.

Avoid: locking on strings or boxed Integers: they may be shared or replaced unexpectedly.

Payments · deadlockMid

Transfers freeze at month end

The transfer service locks the sender account, then the receiver. On salary day it sometimes freezes completely; a thread dump shows two threads BLOCKED, each holding the lock the other needs. Fix it.

The interviewer is checking: deadlock diagnosis and lock ordering.

Show a strong answer
  • A→B and B→A transfers ran at once: thread 1 holds A and wants B; thread 2 holds B and wants A. A circular wait.
  • Lock by a global order (e.g. smaller account id first), whatever the transfer direction.
  • Alternatively use tryLock with a timeout and retry, and alert on long lock waits.

Avoid: restarting the server as the fix. The deadlock will return on the next busy day.

Performance · contentionMid

Thread-safe, but now slow

To fix a race, someone made the whole handleRequest() method synchronized, including a 300 ms database call. Throughput dropped from 800 to 3 requests per second. What would you do?

The interviewer is checking: minimal critical sections.

Show a strong answer
  • The lock serialises every request, so all requests queue behind the slow call.
  • Identify the actual shared state, and lock only the few lines that touch it; do I/O outside the lock.
  • Use concurrent data structures (ConcurrentHashMap, atomics) where they fit, so most code needs no lock.

Avoid: removing synchronization entirely to regain speed; the race comes back.