Threads and ExecutorService
A thread is an independent path of execution. You can start one with new Thread(runnable).start(), but real code uses an ExecutorService (Java 5), a managed pool of threads.
Runnable: a task with no result.Callable<T>: a task that returns a value and may throw.Future<T>: a handle to a pending result;get()waits for it.
Thread states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED.
Always shut executors down. Since Java 19, ExecutorService is AutoCloseable, so try-with-resources does it for you.
Creating threads
Give a Thread a Runnable (often a lambda) and call start(), which runs it on a new thread. Calling run() directly just runs it on the current thread, a classic mistake.
Thread worker = new Thread(() -> System.out.println("Working on " + Thread.currentThread().getName()));
worker.start(); // new thread
// worker.run(); // same thread: not concurrent
Thread vt = Thread.ofVirtual().start(() -> fetchPrices()); // Java 21 virtual threadThread states
A thread moves through NEW โ RUNNABLE โ (BLOCKED, WAITING, TIMED_WAITING) โ TERMINATED. A thread dump (jcmd <pid> Thread.print) shows each thread's state, which is how you find stuck or deadlocked code.
sleep, join and interrupt
sleep pauses the current thread; join waits for another thread to finish. interrupt() politely asks a thread to stop: blocking calls then throw InterruptedException. Handle it by stopping, and restore the flag if you can't rethrow.
Thread t = new Thread(() -> {
try {
while (true) { doWork(); Thread.sleep(1000); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // keep the flag, then exit
}
});
t.start();
t.interrupt();
t.join();Daemon and user threads
The JVM exits when all user threads finish; daemon threads (background helpers) don't keep it alive. Virtual threads are always daemon threads.
ExecutorService and thread pools
Creating threads by hand doesn't scale. An ExecutorService reuses a pool of threads and queues tasks. Always shut it down; since Java 19 it's AutoCloseable, so try-with-resources waits for tasks and closes it.
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
for (String url : urls) pool.submit(() -> download(url));
} // waits for all tasks, then shuts down
try (var perTask = Executors.newVirtualThreadPerTaskExecutor()) {
urls.forEach(u -> perTask.submit(() -> download(u)));
}Callable and Future
A Callable returns a value (and may throw). Submitting it gives a Future; get() waits for the result, preferably with a timeout. invokeAll runs a batch and waits for all.
Future<Integer> count = pool.submit(() -> countLines(path));
try {
int lines = count.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
count.cancel(true);
}Scheduled tasks
A ScheduledExecutorService runs tasks after a delay or repeatedly. In Spring Boot, @Scheduled does the same with less code.
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(this::refreshCache, 0, 10, TimeUnit.MINUTES);Example
try (ExecutorService pool = Executors.newFixedThreadPool(4)) { // Java 19+: auto-close
Future<Integer> price = pool.submit(() -> fetchPrice("SPRING-101"));
Future<Integer> seats = pool.submit(() -> fetchSeats("SPRING-101"));
System.out.println(price.get() + " / " + seats.get(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the interrupt flag
} catch (ExecutionException | TimeoutException e) {
log.error("Lookup failed", e);
}Common mistake
Calling thread.run() instead of thread.start(). run() executes on the current thread, so nothing happens in parallel.
Under the hood
Platform threads map one-to-one to OS threads and each reserves around 1 MB of stack, which is why pools are sized: roughly the number of cores for CPU-bound work, more for I/O-bound work. newCachedThreadPool can create unbounded threads under load; in production prefer a ThreadPoolExecutor with a bounded queue and a rejection policy. In Spring, configure a ThreadPoolTaskExecutor for @Async.
Check yourself
Which method actually starts a new thread?
How this connects
Know these first
Part of Crack the Java interview.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.