What are the ways to create a thread in Java?
Extend
Thread, pass aRunnableto aThread, or submit aRunnableorCallableto anExecutorService. Prefer the executor: it reuses threads, and aCallablecan return a value.ExecutorServiceisAutoCloseablesince Java 19.Javaimport java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) throws Exception { Thread t = new Thread(() -> System.out.println("runnable in a thread")); t.start(); t.join(); // wait for it to finish try (ExecutorService pool = Executors.newFixedThreadPool(2)) { Future<Integer> f = pool.submit(() -> 6 * 7); // a Callable System.out.println("callable returned " + f.get()); } } }Follow-up: what does calling
run()instead ofstart()do? It runs the code on the current thread.What are the states of a thread in Java (the thread lifecycle)?
Six
Thread.Statevalues:NEW,RUNNABLE(running or ready),BLOCKED(waiting for a monitor),WAITING(wait(),join()orpark()with no timeout),TIMED_WAITING(with a timeout, orsleep) andTERMINATED.Javaimport java.util.concurrent.CountDownLatch; public class Main { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Thread t = new Thread(() -> { try { latch.await(); } catch (InterruptedException e) { } }); System.out.println(t.getState()); t.start(); while (t.getState() != Thread.State.WAITING) Thread.onSpinWait(); System.out.println(t.getState()); latch.countDown(); t.join(); System.out.println(t.getState()); } }It prints
NEW,WAITINGandTERMINATED. A thread waiting on aReentrantLockisWAITING, becauseBLOCKEDis only for monitors.What is a race condition and how does
synchronizedor AtomicInteger fix it?A race condition is a result that depends on thread timing:
count++is read, add, write, and two threads can interleave the steps and lose updates.synchronizedadmits one thread at a time and makes its writes visible to the next lock holder;AtomicIntegerdoes it for one counter with lock-free CAS.Javaimport java.util.concurrent.atomic.AtomicInteger; public class Main { private static int syncCount = 0; private static final AtomicInteger atomicCount = new AtomicInteger(); private static synchronized void increment() { syncCount++; } public static void main(String[] args) throws InterruptedException { Runnable work = () -> { for (int i = 0; i < 10_000; i++) { increment(); atomicCount.incrementAndGet(); } }; Thread t1 = new Thread(work), t2 = new Thread(work); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(syncCount + " " + atomicCount.get()); } }It always prints
20000 20000; with a plain unsynchronizedintit usually prints less.What does
volatiledo in Java? Does it makecount++thread safe?volatileguarantees visibility and ordering: every read sees the latest write, and accesses are not reordered across it. It does not makecount++atomic.Use it for a stop flag one thread sets and another polls (without it the JIT may hoist the read and loop forever), and on the instance field in double-checked locking. Counters need
AtomicInteger.What is the difference between
wait()andsleep()?sleep()pauses the thread and keeps its locks.wait()must be called while holding the object's monitor, releases it, and resumes onnotify(), a timeout or an interrupt.Call
wait()in awhileloop that rechecks the condition, because spurious wakeups happen.BlockingQueueorCountDownLatchare easier in new code.What is a deadlock and how do you prevent it?
A deadlock is threads each holding a lock another needs, so all wait forever: thread 1 holds A and wants B while thread 2 holds B and wants A.
Prevent it by taking locks in one global order (by account id in a transfer), or with
tryLock(timeout)and back-off.jstack <pid>reports "Found one Java-level deadlock".What is CompletableFuture and how is it different from Future?
Futureonly has a blockingget().CompletableFuture(Java 8) chains async steps without blocking:thenApply,thenCompose,thenCombine,exceptionally,handle.Javaimport java.util.concurrent.CompletableFuture; public class Main { public static void main(String[] args) { CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> 120); CompletableFuture<Integer> tax = CompletableFuture.supplyAsync(() -> 18); int total = price.thenCombine(tax, Integer::sum) // combine two .thenApply(t -> t * 2) // transform .exceptionally(e -> -1) // fallback on error .join(); System.out.println("total " + total); String failed = CompletableFuture.supplyAsync(() -> { if (true) throw new IllegalStateException("down"); return "ok"; }) .handle((v, e) -> e == null ? v : "recovered: " + e.getCause().getMessage()) .join(); System.out.println(failed); } }It prints
total 276andrecovered: down;handlereceives aCompletionException, hencegetCause(). Async steps run on the commonForkJoinPool(a new thread per task if its parallelism is below two) unless you pass an executor, which you should for blocking I/O.What are virtual threads in Java 21?
Virtual threads (final in Java 21) are JVM-managed threads that unmount from their carrier platform thread while blocked on I/O, so millions can run plain thread-per-request code.
Javaimport java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws Exception { try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<Integer>> results = new ArrayList<>(); for (int i = 1; i <= 10_000; i++) { int n = i; results.add(ex.submit(() -> { Thread.sleep(10); // blocking is cheap here return n; })); } long sum = 0; for (Future<Integer> f : results) sum += f.get(); System.out.println("sum " + sum); } } }It prints
sum 50005000, and 10,000 sleeping tasks finish quickly. They help I/O-bound work, not CPU-bound work, and should not be pooled. In Java 21 blocking insidesynchronizedpins the carrier; JDK 24 removed most of that pinning with JEP 491.How do you solve the producer-consumer problem in Java?
Use a
BlockingQueue:put()waits while the queue is full andtake()waits while it is empty, so the queue does the locking and signalling that hand-writtenwait()andnotify()code often gets wrong.Javaimport java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class Main { public static void main(String[] args) throws InterruptedException { BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2); // holds 2 items Thread producer = new Thread(() -> { try { for (int i = 1; i <= 5; i++) queue.put(i); // waits while full queue.put(-1); // poison pill: done } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread consumer = new Thread(() -> { try { int sum = 0; for (int item; (item = queue.take()) != -1; ) sum += item; // waits while empty System.out.println("consumed sum " + sum); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); } }It prints
consumed sum 15. The-1is a poison pill that tells the consumer to stop, and the bounded capacity stops a fast producer from filling memory. Follow-up: withwait(), test the condition in awhileloop, not anif, because of spurious wakeups.What is
ThreadLocal, and when should you use it?ThreadLocalgives each thread its own copy of a variable, so nothing is shared and no lock is needed. Typical uses are per-request context (user, transaction, trace id) and objects that are not thread safe, such asSimpleDateFormat.Javaimport java.util.Map; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; public class Main { private static final ThreadLocal<StringBuilder> buffer = ThreadLocal.withInitial(StringBuilder::new); private static final Map<String, String> seen = new ConcurrentHashMap<>(); static void log(String msg) { buffer.get().append(msg).append(' '); } public static void main(String[] args) throws InterruptedException { Runnable job = () -> { String name = Thread.currentThread().getName(); log(name + "-a"); log(name + "-b"); seen.put(name, buffer.get().toString().trim()); buffer.remove(); // clean up, above all on pooled threads }; Thread t1 = new Thread(job, "T1"), t2 = new Thread(job, "T2"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(new TreeMap<>(seen)); } }It prints
{T1=T1-a T1-b, T2=T2-a T2-b}: each thread saw only its own buffer. Callremove()when the work ends. Pool threads live as long as the application, so a forgotten value leaks memory and can hand one request's data to the next.
Multithreading in Java Interview Questions
Threads, synchronization, volatile, executors, CompletableFuture and virtual threads.
10 questions