Threads

Java threads — creating and managing threads, thread lifecycle, and the concurrency problems every enterprise developer must understand.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: A thread is the smallest unit of execution in Java. Every Java program starts with the main thread. You create additional threads to do work concurrently — loading data while the UI responds, processing multiple requests simultaneously, running background tasks. Java 21 introduced virtual threads, which allow millions of lightweight threads without OS overhead.

🔍 What Is It?

Thread Lifecycle:

NEW -> RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING -> TERMINATED
  • →NEW — Thread created but not started
  • →RUNNABLE — Running or ready to run
  • →BLOCKED — Waiting to acquire a monitor lock
  • →WAITING — Waiting indefinitely (wait(), join())
  • →TIMED_WAITING — Waiting with timeout (sleep(), wait(timeout))
  • →TERMINATED — Completed execution

Creating Threads:

  1. Extend Thread class (not recommended)
  2. Implement Runnable (better — separates task from execution)
  3. Use ExecutorService (best — thread pool management)
  4. Virtual threads (Java 21 — for I/O-bound tasks)

Thread Properties:

  • →name — for debugging (set meaningful names!)
  • →daemon — daemon threads stop when all non-daemon threads finish
  • →priority — hints to scheduler (1-10, rarely useful)
  • →interrupt — mechanism to request thread stop

🎯 Why Do We Need It?

Threads enable concurrency:

  • →Server applications — handle thousands of simultaneous HTTP requests
  • →Background tasks — send emails, process files without blocking the main flow
  • →Parallel computation — use all CPU cores for CPU-intensive work
  • →Responsive UIs — keep UI thread free while background work runs
  • →Asynchronous I/O — don't block waiting for network/disk

🏗️ What Happens in Real Projects?

In Spring Boot, threads are mostly managed by the framework:

  • →Each HTTP request runs in a thread from Tomcat's thread pool (default: 200 threads)
  • →@Async methods run in a Spring-managed thread pool
  • →@Scheduled tasks run in a scheduled thread pool
  • →CompletableFuture.supplyAsync() runs in ForkJoinPool

You rarely create threads directly — you use ExecutorService or Spring's abstractions.

⚙️ How It Works

Thread Scheduling:

The JVM delegates thread scheduling to the OS. The OS scheduler decides which thread runs on which CPU core at any time. You cannot control exact scheduling — only set priorities as hints.

Thread Safety Problems:

  1. Race condition — two threads read-modify-write the same variable concurrently; result depends on timing
  2. Deadlock — Thread A holds lock1, waits for lock2; Thread B holds lock2, waits for lock1; both wait forever
  3. Livelock — threads keep responding to each other but make no progress
  4. Starvation — a thread never gets CPU time because others always have priority

Memory Visibility:

Without synchronization, changes made by one thread may not be visible to another thread due to CPU caches. Use volatile, synchronized, or java.util.concurrent for visibility guarantees.

💻 Example

Thread Patterns in Enterprise Java — How threads are used and managed in production Java applications.

java
// Option 1: Extend Thread (avoid — couples task to execution)
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}
new MyThread().start();

// Option 2: Implement Runnable (better — separates task from thread)
Runnable task = () -> System.out.println("Running in: " + 
    Thread.currentThread().getName());
Thread thread = new Thread(task, "my-worker");
thread.start();

// Option 3: ExecutorService (best for most cases)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> processOrder(orderId));
executor.submit(() -> sendEmail(userId));

// Always shutdown executors
executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

// Virtual threads (Java 21) — for I/O-bound tasks
// Create millions of lightweight threads
Thread.ofVirtual().start(() -> handleRequest(request));

// Virtual thread executor
try (ExecutorService vExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        vExecutor.submit(() -> callExternalApi());
    }
}  // auto-shutdown via try-with-resources

🤔 Common Confusions

❓ What is the difference between start() and run()?

start() creates a new thread and calls run() in that new thread. run() just executes the method in the current thread — no new thread is created. Always call start() to actually run code concurrently. Calling run() directly is a common mistake that looks like multithreading but is actually sequential.

❓ What is the difference between volatile and synchronized?

volatile guarantees visibility — changes to a volatile variable are immediately visible to all threads. It does NOT guarantee atomicity — count++ on a volatile int is still a race condition. synchronized guarantees both visibility AND atomicity — only one thread can execute the synchronized block at a time. Use volatile for simple flags (boolean running = true). Use synchronized or AtomicXxx for compound operations.

❓ What are virtual threads and how are they different from regular threads?

Regular (platform) threads are OS threads — expensive to create (1MB stack each), limited to thousands. Virtual threads (Java 21) are JVM-managed, extremely lightweight — you can create millions. When a virtual thread blocks on I/O, the JVM parks it and runs another virtual thread on the same OS thread. This eliminates the need for reactive/async programming for I/O-bound code.

🚨 Common Production Issues

Deadlock — two threads waiting for each other's locks. Diagnose with jstack. Fix: always acquire locks in the same order.

Thread pool exhaustion — all threads busy, new requests queue up and timeout. Monitor thread pool utilization. Increase pool size or use virtual threads.

ThreadLocal memory leak — ThreadLocal values in thread pools persist across requests if not cleaned up. Always call ThreadLocal.remove() in a finally block.

Uncaught exceptions in threads — exceptions in threads not caught by the caller. Set an UncaughtExceptionHandler or use CompletableFuture which propagates exceptions.

✅ Best Practices

Never create threads directly in production code — use ExecutorService or Spring @Async.

Always name threads meaningfully for debugging: new Thread(task, "order-processor-1").

Use AtomicInteger/AtomicLong instead of synchronized for simple counters.

Use virtual threads (Java 21) for I/O-bound tasks to eliminate thread pool sizing concerns.

Always handle InterruptedException: catch it, restore the interrupt flag, and exit gracefully.

Set UncaughtExceptionHandler on thread pools to log unexpected exceptions.

🏛️ Architect's Note

Senior Architect's Perspective

Java 21 virtual threads are the biggest threading change in Java history. For I/O-bound microservices (most enterprise services), virtual threads eliminate the need for reactive programming (WebFlux, RxJava). You write simple blocking code, but the JVM handles concurrency efficiently. Spring Boot 3.2+ supports virtual threads with spring.threads.virtual.enabled=true. For CPU-bound tasks, platform threads with ForkJoinPool are still the right choice.

💬 Frequently Asked Questions

What is the difference between a daemon thread and a user thread?

User threads (default) keep the JVM alive — the JVM waits for all user threads to finish before exiting. Daemon threads are background service threads — the JVM exits when only daemon threads remain. Set daemon status before starting: thread.setDaemon(true). Use daemon threads for background tasks (log flushing, GC helpers) that should not prevent JVM shutdown.

What is a thread dump and how do I read it?

A thread dump is a snapshot of all thread states at a moment in time. Generate with jstack <pid> or kill -3 <pid>. Look for: BLOCKED threads (waiting for a lock held by another thread — indicates deadlock or contention), WAITING/TIMED_WAITING threads (parked, normal), many threads in the same method (bottleneck). Tools like FastThread.io visualize thread dumps automatically.

🎯 Key Takeaways

1Thread lifecycle: NEW -> RUNNABLE -> BLOCKED/WAITING -> TERMINATED
2Always use ExecutorService, not raw Thread creation in production
3volatile = visibility only; synchronized = visibility + atomicity
4AtomicInteger/LongAdder are faster than synchronized for counters
5Java 21 virtual threads: millions of lightweight threads for I/O-bound code
6Name threads and set UncaughtExceptionHandler for production debugging

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.