Multithreading and Concurrency · Lesson 39 of 42
Threads, Race Conditions and synchronized
A thread is an independent path of execution inside a program. A web server handles each request on its own thread; an app downloads a file in the background while the screen stays responsive. Threads make programs faster and more responsive – and introduce a new class of bugs. This lesson covers both.
In this lesson
- Creating threads with
Runnableand lambdas start()vsrun(),join()and thread states- Race conditions – and three ways to fix them
- Deadlock and the rules that avoid it
Starting threads
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
String name = Thread.currentThread().getName();
int sum = 0;
for (int i = 1; i <= 1000; i++) sum += i;
System.out.println(name + " computed " + sum);
};
Thread t1 = new Thread(task, "worker-1");
Thread t2 = new Thread(task, "worker-2");
t1.start(); // runs in parallel with main
t1.join(); // wait for worker-1 to finish
t2.start();
t2.join();
System.out.println("main done");
}
}
worker-1 computed 500500 worker-2 computed 500500 main done
- Put the work in a
Runnable(usually a lambda) and pass it to aThread. ExtendingThreadalso works but wastes your one chance at inheritance. start()creates a new thread that callsrun(). Callingrun()directly just runs the code on the current thread – a classic interview trap.join()waits for a thread to finish. Without it, the order of output from different threads is unpredictable.
Thread states
Thread lifecycle
- NEWcreated, start() not called yet
- RUNNABLErunning or ready to run
- BLOCKED / WAITING / TIMED_WAITINGwaiting for a lock, join(), sleep()…
- TERMINATEDrun() finished
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
System.out.println(t.getState());
t.start();
Thread.sleep(50);
System.out.println(t.getState());
t.join();
System.out.println(t.getState());
}
}
NEW TIMED_WAITING TERMINATED
A race condition
When threads share data and at least one writes it, results can depend on timing. count++ looks like one step but is really three: read, add, write. Two threads can read the same old value, and one update is lost.
public class Main {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable add = () -> { for (int i = 0; i < 100_000; i++) count++; };
Thread a = new Thread(add), b = new Thread(add);
a.start(); b.start();
a.join(); b.join();
System.out.println("expected 200000, got " + count);
}
}
expected 200000, got 137482
How an update is lost (count = 5)
- Thread A reads 5…
- Thread B reads 5before A writes
- A writes 6…
- B writes 6two increments, but count is 6, not 7
Three ways to fix it
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
static int syncCount = 0;
static int lockCount = 0;
static final AtomicInteger atomicCount = new AtomicInteger();
static final ReentrantLock lock = new ReentrantLock();
static synchronized void incrementSync() { syncCount++; } // 1. synchronized method
static void incrementLock() { // 2. explicit lock
lock.lock();
try { lockCount++; } finally { lock.unlock(); }
}
public static void main(String[] args) throws InterruptedException {
Runnable work = () -> {
for (int i = 0; i < 100_000; i++) {
incrementSync();
incrementLock();
atomicCount.incrementAndGet(); // 3. atomic variable
}
};
Thread a = new Thread(work), b = new Thread(work);
a.start(); b.start();
a.join(); b.join();
System.out.println(syncCount + " " + lockCount + " " + atomicCount.get());
}
}
200000 200000 200000
| Tool | Use when |
|---|---|
synchronized | simple mutual exclusion on a method or block (synchronized (obj) { … }) |
ReentrantLock | you need tryLock, timeouts or fairness; always unlock in finally |
AtomicInteger, AtomicLong | a single counter or value – fastest, lock-free |
ConcurrentHashMap, CopyOnWriteArrayList | shared collections |
Interview angle: volatile
volatile makes a variable's latest value visible to all threads (no stale cached copies), but it does not make count++ atomic. Use it for simple flags such as volatile boolean running; use atomics or locks for read-modify-write updates.
Deadlock
A deadlock happens when thread 1 holds lock A and waits for lock B, while thread 2 holds B and waits for A – both wait forever. Avoid it by:
- always acquiring multiple locks in the same order everywhere;
- holding locks for as short a time as possible;
- using
tryLock(timeout)so a thread can give up and retry; - preferring higher-level tools (executors, concurrent collections) over hand-written locking.
Common mistake: synchronizing on different objects
synchronized instance methods lock this. If two threads use two different objects, they don't block each other at all, and a shared static field is still unprotected. Lock on the object that actually owns the shared data.
Practice
Try it yourself
Use 4 threads to sum the numbers 1–4,000 in four ranges, adding into an
AtomicLong.Show solution
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; public class Main { public static void main(String[] args) throws InterruptedException { AtomicLong total = new AtomicLong(); List<Thread> threads = new ArrayList<>(); for (int t = 0; t < 4; t++) { int from = t * 1000 + 1, to = (t + 1) * 1000; Thread th = new Thread(() -> { long sum = 0; for (int i = from; i <= to; i++) sum += i; total.addAndGet(sum); }); threads.add(th); th.start(); } for (Thread th : threads) th.join(); System.out.println(total.get()); } }8002000
Quick check
What is the difference between start() and run()?
start() creates a new thread that runs run(); calling run() directly executes it on the current thread.
Why is count++ not thread-safe?
It is three steps (read, add, write); threads can interleave and lose updates.
Does volatile make count++ safe?
No – it only guarantees visibility. Use AtomicInteger or synchronization.
Summary
- Create threads from Runnables, call start(), and join() to wait.
- Shared mutable data causes race conditions; protect it with synchronized, locks or atomics.
- Avoid deadlock with consistent lock order and short critical sections.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.