Threads and multithreading, creating threads, Thread vs Runnable, the thread lifecycle, starting twice, synchronized, volatile, wait/notify, thread safety, the Java Memory Model, Exchanger, and a server without frameworks.
Published September 25, 2026
Freshers get the basic version of concurrency questions: what a thread is, how to create one, synchronized and volatile. Answer precisely: "volatile makes things thread-safe" is the kind of answer that loses marks. Deeper concurrency (executors, locks, CompletableFuture) comes in the 2–5 year tier.
Short answer: A thread is an independent path of execution inside a process: the smallest unit of work the operating system schedules. Multithreading means running several threads in one program at the same time, so work proceeds concurrently. That can be truly in parallel on multi-core CPUs, or interleaved on a single core.
Key points to cover:
Learn it in depth → Introduction to Java Threads
Short answer: You give a Thread a task and call start(). There are three ways to supply the task:
Runnable (or use a lambda).Thread.ExecutorService, which is the preferred way in real applications.Runnable task = () -> System.out.println("sending email on " + Thread.currentThread().getName());
new Thread(task).start(); // 1. Runnable
class Worker extends Thread { public void run() { /* … */ } }
new Worker().start(); // 2. extending Thread (least flexible)
try (ExecutorService pool = Executors.newFixedThreadPool(4)) { // 3. thread pool (Java 19+: AutoCloseable)
pool.submit(task);
}
Thread.ofVirtual().start(task); // Java 21: a virtual thread
Common trap: calling run() instead of start(). run() just executes the method on the current thread, and no new thread is created.
Learn it in depth → ExecutorService
Thread class and the Runnable interface?Short answer: Runnable represents the task (what to run). Thread represents the worker that runs it. Implementing Runnable is preferred: your class stays free to extend another class, the task stays separate from the threading mechanism, and the same task can be run by a thread pool.
Key points to cover:
Callable<V> is the richer alternative. It returns a result and can throw checked exceptions. Submit it to an executor, and get a Future<V> back.Short answer: A thread moves through the Thread.State values:
start() not called yet.wait(), join() or LockSupport.park().sleep(ms), wait(ms) or join(ms).run() has finished.NEW --start()--> RUNNABLE --run() ends--> TERMINATED
| ^
lock busy v | lock acquired / notified / timeout
BLOCKED / WAITING / TIMED_WAITING
Key points to cover:
Short answer: No. Calling start() a second time on the same Thread object throws an IllegalThreadStateException, even after it has finished. To run the task again, create a new Thread, or submit the task to an executor again.
synchronized keyword do?Short answer: It ensures that only one thread at a time executes a block or method guarded by the same monitor lock. It also guarantees visibility: changes made before releasing the lock are seen by the next thread that acquires it.
class Counter {
private int count;
public synchronized void increment() { count++; } // lock = this object
public synchronized int get() { return count; }
}
class Inventory {
private final Object lock = new Object();
private int stock;
void reserve(int qty) {
synchronized (lock) { // lock only the critical section
if (stock < qty) throw new IllegalStateException("out of stock");
stock -= qty;
}
}
}
Key points to cover:
this. A synchronized static method locks the Class object.Learn it in depth → Synchronized and Locks
volatile?Short answer: volatile guarantees visibility and ordering for a single variable. A write by one thread is immediately visible to other threads that read it, and the compiler and CPU can't reorder operations around it in ways that break that guarantee. It does not make compound operations such as count++ atomic.
class Worker implements Runnable {
private volatile boolean running = true; // without volatile, the loop might never see the update
public void run() { while (running) { doWork(); } }
public void stop() { running = false; }
}
Key points to cover:
AtomicInteger, or synchronized.Common trap: "volatile reads from main memory instead of the cache". That's a simplification. CPU caches are coherent. What volatile really provides is a happens-before relationship, and a ban on reordering.
Learn it in depth → Volatile and the Java Memory Model
Short answer: The options, roughly from best to worst:
AtomicInteger, ConcurrentHashMap, BlockingQueue.synchronized or ReentrantLock when several fields must change together.volatile for simple flags.Key points to cover:
Short answer: Make the updates atomic with respect to each other. Either use a concurrent structure (ConcurrentHashMap.merge, ConcurrentLinkedQueue), or guard every access to the shared structure with the same lock.
Map<String, Integer> views = new ConcurrentHashMap<>();
views.merge(pageId, 1, Integer::sum); // atomic per key: no lost updates
Common trap: synchronising the writes but not the reads, or using different locks for different methods. Every access to the shared state must go through the same lock.
Short answer: Synchronisation coordinates threads' access to shared state. It prevents race conditions (lost updates, inconsistent reads) and ensures visibility of changes between threads. Without it, count++ run by two threads can lose increments, because it's really three steps: read, add, write.
wait() and notify()?Short answer: For one thread to wait for a condition that another thread will make true. The classic example is producer-consumer: the consumer waits while the queue is empty, and the producer notifies it after adding an item. Both must be called while holding the object's monitor.
synchronized (queue) {
while (queue.isEmpty()) queue.wait(); // always wait in a loop: spurious wakeups happen
item = queue.poll();
}
// producer
synchronized (queue) { queue.add(item); queue.notifyAll(); }
Key points to cover:
IllegalMonitorStateException.BlockingQueue, CountDownLatch, or CompletableFuture instead of hand-written wait/notify.Short answer: Race conditions, deadlocks (two threads each waiting for the other's lock), livelock and starvation, visibility bugs (stale values), contention (threads queuing for locks), and bugs that are hard to reproduce, because they depend on timing.
Key points to cover:
tryLock).jstack, jcmd <pid> Thread.print).Learn it in depth → Deadlock, Starvation & Livelock
Short answer: The JMM is the specification of when a write made by one thread becomes visible to another, and which reorderings are allowed. Its central idea is happens-before: unlocking a monitor happens-before the next lock of it, a volatile write happens-before later reads of that variable, and Thread.start() and join() create happens-before edges too.
Key points to cover:
synchronized, volatile, final fields and the java.util.concurrent classes are all built on these rules.Learn it in depth → Volatile and the Java Memory Model
Exchanger class?Short answer: java.util.concurrent.Exchanger<V> is a synchronisation point where two threads swap objects. Each thread calls exchange(myObject), blocks until its partner arrives, and then receives the partner's object.
Exchanger<List<String>> exchanger = new Exchanger<>();
// filler thread: buffer = exchanger.exchange(fullBuffer); // hands over a full buffer, gets an empty one
// drainer thread: buffer = exchanger.exchange(emptyBuffer);
Key points to cover:
BlockingQueue.Learn it in depth → Concurrent Utilities & Coordination
Short answer: Yes. The JDK includes everything you need:
java.net.ServerSocket for a raw TCP server.com.sun.net.httpserver.HttpServer for a simple HTTP server.jwebserver tool for serving static files.HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/health", exchange -> {
byte[] body = "OK".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(body); }
});
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); // one virtual thread per request
server.start();
Key points to cover:
Q: What's the difference between sleep() and wait()?
A: Thread.sleep() pauses the current thread and keeps any locks it holds. wait() must be called while holding the object's monitor, and it releases that monitor until another thread calls notify or notifyAll, or the timeout expires.
Q: What does join() do?
A: It makes the calling thread wait until the target thread finishes, for example to wait for worker threads before combining their results.
Q: What is a daemon thread?
A: A background thread, such as the GC or a housekeeping timer, that doesn't keep the JVM alive. The JVM exits when only daemon threads remain. Mark a thread with setDaemon(true) before starting it.
Q: Process vs thread? A: A process has its own memory space, and is isolated from other processes. Threads live inside a process and share its memory. Creating threads and switching between them is cheaper, but they need synchronisation.
Q: What are virtual threads? A: Lightweight threads (final in Java 21) that are scheduled by the JVM onto a few platform threads. You can have millions of them, which makes simple blocking code scale for I/O-heavy servers.