A process is an independently executing program with its own isolated memory space, file handles, and OS resources — processes don't share memory directly and communicate via IPC (pipes, sockets, shared files). A thread is a unit of execution within a process; all threads in a process share the same heap and static memory, which is what makes concurrent access to shared data a concern (and also what makes inter-thread communication cheap compared to inter-process communication).
Three ways to create a task that runs on a thread:
1. Extend Thread, override run():
class MyThread extends Thread {
public void run() { System.out.println("running"); }
}
new MyThread().start(); // start(), not run() — run() alone just calls the method normally
2. Implement Runnable (preferred — doesn't burn your one shot at extending a class, and separates "what to run" from "how it runs"):
Runnable task = () -> System.out.println("running");
new Thread(task).start();
3. Implement Callable<V> — like Runnable but can return a value and throw checked exceptions, typically submitted to an ExecutorService rather than run directly on a raw Thread:
Callable<Integer> task = () -> { return compute(); };
Future<Integer> future = executor.submit(task);
int result = future.get(); // blocks until done, rethrows checked exceptions wrapped
Best practice: prefer Runnable/Callable submitted to an ExecutorService over manually managing raw Thread objects — it decouples task logic from thread lifecycle/pooling and is far easier to tune and reason about at scale.
Thread.State (queryable via thread.getState()) defines six states:
NEW: theThreadobject has been created butstart()hasn't been called yet.RUNNABLE: the thread is executing, or is eligible to execute and waiting for CPU time from the OS scheduler — Java doesn't distinguish "actually running" from "ready to run" at the language level; both map toRUNNABLE.BLOCKED: the thread is waiting to acquire a monitor lock to enter asynchronizedblock/method that another thread currently holds.WAITING: the thread is waiting indefinitely for another thread to perform a specific action — e.g., it calledObject.wait()(no timeout),Thread.join()(no timeout), orLockSupport.park().TIMED_WAITING: likeWAITING, but bounded by a timeout — e.g.,Thread.sleep(ms),Object.wait(timeout),Thread.join(timeout).TERMINATED: the thread'srun()method has completed (normally or via an uncaught exception); it cannot be restarted.
Thread t = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) {} });
t.getState(); // NEW
t.start();
t.getState(); // RUNNABLE (or briefly TIMED_WAITING once sleep() kicks in)
t.join();
t.getState(); // TERMINATED
A common interview follow-up: BLOCKED vs WAITING — BLOCKED specifically means contending for a lock to enter a synchronized region; WAITING means the thread voluntarily gave up execution and is waiting for a signal (notify()/notifyAll()) or another thread to finish, having already been inside (or without needing) a lock.
Related Resources
synchronized provides mutual exclusion: only one thread can hold a given object's intrinsic lock (monitor) at a time, so code guarded by the same lock never runs concurrently across threads. It also establishes a happens-before relationship — changes made by a thread before releasing the lock are guaranteed visible to the next thread that acquires the same lock, which matters as much as the mutual exclusion itself.
Synchronized instance method — locks on this (the current object):
class Counter {
private int count;
synchronized void increment() { count++; } // locks on `this`
}
Synchronized static method — locks on the Class object itself (shared across all instances):
class Registry {
static synchronized void register(String name) { /* locks on Registry.class */ }
}
Synchronized block — locks on any explicit object you choose, letting you scope the critical section as narrowly as possible (reducing how long the lock is held, and thus contention):
class Cache {
private final Object lock = new Object();
private Map<String, String> data = new HashMap<>();
void put(String k, String v) {
// ... some non-critical work here, unsynchronized ...
synchronized (lock) {
data.put(k, v); // only this part needs the lock
}
}
}
Why prefer blocks over whole-method synchronization: synchronizing an entire method locks for its whole duration, even parts that don't touch shared state — a synchronized block using a dedicated private lock object minimizes contention and avoids accidentally exposing your lock object to external code that could lock on it unexpectedly (a risk when synchronizing on this in a public API).
Both relate to thread visibility of shared state, but solve different problems:
volatile guarantees:
- Visibility: a write to a
volatilefield is immediately visible to all other threads' subsequent reads (it's flushed to/read from main memory rather than a per-CPU cache, roughly speaking). - Ordering: prevents the compiler/JIT/CPU from reordering instructions around the volatile access in ways that would break the visibility guarantee.
It does not guarantee atomicity for compound operations:
volatile int counter = 0;
counter++; // NOT atomic — read, increment, write are 3 separate steps
// two threads can interleave and lose an increment
synchronized guarantees both visibility (via happens-before on lock acquire/release) and atomicity/mutual exclusion — only one thread executes the guarded block at a time, so compound operations like counter++ are safe if wrapped in it:
synchronized (lock) {
counter++; // atomic with respect to other threads using the same lock
}
When volatile is enough: simple flags or single-write-many-read state where you don't need atomic compound updates — e.g., a volatile boolean shutdownRequested flag checked by a worker loop.
When you need synchronized (or java.util.concurrent.atomic classes): anything involving read-modify-write sequences (counters, "check-then-act" logic) where multiple threads might interleave mid-operation.
volatile is also cheaper — it has no lock acquisition/contention overhead, just a memory barrier — but that only matters if it actually fits the visibility-only use case.
Deadlock happens when two (or more) threads are each waiting for a resource the other holds, so none of them can ever proceed:
Object lockA = new Object(), lockB = new Object();
// Thread 1
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { /* ... */ } // waits for lockB, held by Thread 2
}
// Thread 2
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { /* ... */ } // waits for lockA, held by Thread 1
}
// Both threads block forever — classic deadlock
The four classic necessary conditions (Coffman conditions) are: mutual exclusion, hold-and-wait, no preemption, and circular wait — breaking any one prevents deadlock.
Prevention strategies:
- Fixed lock ordering: always acquire multiple locks in the same globally-agreed order across all threads (e.g., by a stable ID). If both threads above always locked
lockAbeforelockB, deadlock becomes impossible — this breaks the "circular wait" condition. - Lock timeouts: use
Lock.tryLock(timeout)fromjava.util.concurrent.locksinstead of an unconditional blocking acquire — if a thread can't get the second lock within the timeout, it backs off, releases what it holds, and retries, breaking "hold-and-wait." - Minimize lock scope and count: hold locks for as short a time as possible, and avoid nesting multiple locks when a single, coarser lock (or a lock-free structure) would do.
- Prefer higher-level concurrency utilities:
java.util.concurrentcollections (ConcurrentHashMap),java.util.concurrent.atomicclasses, and executor-based designs often avoid the need for manual multi-lock coordination entirely.
Detecting an existing deadlock in production: a thread dump (jstack <pid>) explicitly reports "Found one Java-level deadlock" with the involved threads and locks.