Java Threads: start vs run, Race Conditions, synchronized and volatile
Codevity Admin · 10/25/2026
Step 24 of 38 · Java Interview Preparation for Beginners · Stage 5: Concurrency and the JVM
Codevity Admin · 10/25/2026
Concurrency is where fresher interviews turn from definitions into reasoning. You are rarely asked to build something large. You are asked to spot why a small program is wrong, and the usual answer is one of three words: race, visibility or deadlock.
Implement Runnable (or pass a lambda) and hand it to a Thread:
public class StartVsRun {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(
() -> System.out.println("running in " + Thread.currentThread().getName()), "worker-1");
worker.run(); // a plain method call: prints "running in main"
worker.start(); // starts a new thread: prints "running in worker-1"
worker.join(); // wait for the worker to finish
}
}start() asks the JVM to create a new thread that then calls run(). Calling run() yourself just executes the method on the current thread, with no concurrency at all. A thread can be started only once: a second start() throws IllegalThreadStateException.
Prefer Runnable over extending Thread: it separates the task from how it runs, and Java has single inheritance.
counter++ is really three steps: read, add one, write. Two threads can both read the same value and each write back the same result, so one increment is lost.
import java.util.concurrent.atomic.AtomicInteger;
public class RaceCondition {
static int unsafe = 0;
static int guarded = 0;
static final Object LOCK = new Object();
static final AtomicInteger atomic = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable work = () -> {
for (int i = 0; i < 100_000; i++) {
unsafe++; // lost updates are possible
synchronized (LOCK) { guarded++; } // only one thread at a time
atomic.incrementAndGet(); // a single atomic operation
}
};
Thread a = new Thread(work);
Thread b = new Thread(work);
a.start();
b.start();
a.join();
b.join();
System.out.println(unsafe <= 200_000); // true (usually lower, and different on each run)
System.out.println(guarded); // 200000
System.out.println(atomic.get()); // 200000
}
}The output for unsafe is unpredictable, which is exactly why this bug survives testing and appears in production. join() also guarantees that the main thread sees everything the other threads wrote before they ended.
| Tool | Use for | Notes |
|---|---|---|
synchronized block or method | Making a compound action atomic and visible | Reentrant, released automatically even on exceptions |
ReentrantLock | tryLock, timeouts, fairness, interruptible waits | You must call unlock() in finally |
AtomicInteger, AtomicLong, AtomicReference | A single variable updated by many threads | Lock-free, uses compare-and-swap |
volatile | A flag or a reference read by many threads | Gives visibility, not atomicity |
| Immutable objects | Sharing data without any locking | The simplest thread-safe design |
Without synchronization, one thread may never see a change made by another, because values can be cached in registers or per-core caches. A volatile variable guarantees that every read sees the latest write and that the compiler does not reorder around it.
public class StopFlag {
private static volatile boolean running = true; // remove volatile and the loop may never end
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long ticks = 0;
while (running) {
ticks++;
}
System.out.println("worker saw the flag change");
});
worker.start();
Thread.sleep(100);
running = false; // visible to the worker thanks to volatile
worker.join();
System.out.println("main finished"); // main finished
}
}volatile int count; count++; is still a race, because the increment is three steps. Use AtomicInteger for that.
A deadlock needs four things at once: mutual exclusion, hold-and-wait, no preemption, and a circular wait. The classic case is two threads taking the same two locks in opposite orders:
// Illustrative: this program can hang forever, so do not run it.
public class Deadlock {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) {
new Thread(() -> { synchronized (A) { pause(); synchronized (B) { } } }).start();
new Thread(() -> { synchronized (B) { pause(); synchronized (A) { } } }).start();
}
static void pause() {
try { Thread.sleep(100); } catch (InterruptedException ignored) { }
}
}The fix is to break the circular wait by always acquiring locks in the same global order. With a consistent order the program always finishes:
Other ways out: , keeping critical sections small, and avoiding calls to unknown code while holding a lock. To investigate a hung production process, take a thread dump ( or ), which reports "Found one Java-level deadlock".
Hand-written and are easy to get wrong (they must be called inside , in a loop that rechecks the condition). Use a , which does the waiting for you: blocks when the queue is full, blocks when it is empty.
| Method | What it does |
|---|---|
| Pauses the current thread and keeps any locks it holds | |
| Releases the monitor and waits until or . Must be called inside | |
| Waits for another thread to finish | |
| Requests that a thread stop. Blocking calls throw . Restore the flag with if you cannot handle it |
(created) then (running or ready), then (waiting for a monitor), (, , ), (, ) and finally .
ReentrantLock.tryLock(timeout)jstack <pid>jcmd <pid> Thread.printwait()notify()synchronizedBlockingQueueputtakeThread.sleep(ms)Object.wait()notify()notifyAll()synchronizedthread.join()thread.interrupt()InterruptedExceptionThread.currentThread().interrupt()NEWRUNNABLEBLOCKEDWAITINGwaitjoinparkTIMED_WAITINGsleep(ms)wait(ms)TERMINATEDsynchronizedvolatileHashMapConcurrentHashMapvolatile intRaceConditionLockOrderingpublic class LockOrdering {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
synchronized (A) { // everyone takes A first, then B
synchronized (B) {
System.out.println("both locks held by " + Thread.currentThread().getName());
}
}
};
Thread t1 = new Thread(task, "t1");
Thread t2 = new Thread(task, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("no deadlock"); // no deadlock
}
}import java.util.concurrent.*;
public class ProducerConsumer {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);
int[] total = {0};
Thread consumer = new Thread(() -> {
try {
while (true) {
int n = queue.take();
if (n < 0) break; // a negative number is the "poison pill": stop
total[0] += n;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
consumer.start();
for (int i = 1; i <= 5; i++) {
queue.put(i); // blocks while the queue already holds 2 items
}
queue.put(-1);
consumer.join();
System.out.println(total[0]); // 15
}
}