Chapter 06 · Article 32 of 55
Creating and Managing Threads
Threads are the fundamental units of execution within a process. Creating and managing threads effectively is critical for building responsive, high-performance systems. This ar…
Article outline14 sections on this page+
Overview
Threads are the fundamental units of execution within a process. Creating and managing threads effectively is critical for building responsive, high-performance systems. This article covers the complete lifecycle of a thread - from creation through termination - along with coordination mechanisms that allow threads to work together safely.
Key concerns when working with threads:
- Creation: How to spawn new threads and assign work to them
- Lifecycle Management: Controlling thread states - starting, pausing, resuming, and stopping
- Coordination: Ensuring threads cooperate without corrupting shared state or deadlocking
- Resource Management: Preventing thread leaks, managing thread counts, and cleaning up properly
Understanding these fundamentals is prerequisite to higher-level concurrency constructs like thread pools, executors, and concurrent data structures.
Thread Lifecycle
Every thread passes through a well-defined set of states from birth to death:
┌─────────────────────────────────┐
│ │
▼ │
┌─────┐ start() ┌──────────┐ scheduler ┌─────────┐│
│ NEW │─────────────▶│ RUNNABLE │──────────────▶│ RUNNING ││
└─────┘ └──────────┘◀──────────────└─────────┘│
▲ time slice expired │ │
│ │ │
│ notify()/ │ │
│ timeout/ │ │
│ lock acquired │ │
│ │ │
┌────┴──────────────────┐ │ │
│ BLOCKED / WAITING / │◀─────────┘ │
│ TIMED_WAITING │ sleep()/ │
└───────────────────────┘ wait()/ │
join()/ │
lock │
┌────────────┐ │
│ TERMINATED │◀──────────────────┘
└────────────┘ run() completes
or exception
State Descriptions
| State | Description |
|---|---|
| NEW | Thread object created but start() not yet called. No system resources allocated. |
| RUNNABLE | Thread is eligible to run. Waiting for CPU time from the scheduler. |
| RUNNING | Thread is actively executing on a CPU core. |
| BLOCKED | Thread is waiting to acquire a monitor lock held by another thread. |
| WAITING | Thread is waiting indefinitely for another thread's signal (wait(), join() with no timeout). |
| TIMED_WAITING | Thread is waiting with a timeout (sleep(ms), wait(ms), join(ms)). |
| TERMINATED | Thread has completed execution - either normally or via an unhandled exception. Cannot be restarted. |
Key Transitions
- NEW → RUNNABLE: Calling
start()allocates OS-level resources and places the thread in the ready queue. - RUNNABLE → RUNNING: The OS scheduler selects this thread for execution.
- RUNNING → RUNNABLE: Time slice expires or
yield()is called voluntarily. - RUNNING → BLOCKED: Thread attempts to enter a synchronized block held by another thread.
- RUNNING → WAITING: Thread calls
wait()orjoin()without timeout. - RUNNING → TIMED_WAITING: Thread calls
sleep(ms)orwait(ms). - WAITING/TIMED_WAITING → RUNNABLE: Notification received, timeout expires, or interrupted.
- RUNNING → TERMINATED:
run()method returns or throws an uncaught exception.
Creating Threads
Approach 1: Extending the Thread Class
class FileProcessor extends Thread:
field filePath
constructor(filePath):
this.filePath = filePath
method run():
data = readFile(filePath)
process(data)
print("Processed: " + filePath)
// Usage
thread = new FileProcessor("/data/input.csv")
thread.start()
Approach 2: Implementing the Runnable Interface
class DataCruncher implements Runnable:
field dataset
constructor(dataset):
this.dataset = dataset
method run():
result = computeStatistics(dataset)
print("Stats computed: " + result)
// Usage
task = new DataCruncher(myData)
thread = new Thread(task)
thread.start()
Approach 3: Implementing Callable (with Return Value)
class PriceCalculator implements Callable<Double>:
field items
constructor(items):
this.items = items
method call() -> Double:
total = 0.0
for item in items:
total += item.price * item.quantity
return total
// Usage
calculator = new PriceCalculator(cartItems)
future = executorService.submit(calculator)
totalPrice = future.get() // blocks until result is ready
Comparison Table
| Aspect | Extend Thread | Implement Runnable | Implement Callable |
|---|---|---|---|
| Inheritance | Consumes single inheritance | Allows extending other classes | Allows extending other classes |
| Return value | No | No | Yes (via Future) |
| Exception handling | Cannot throw checked exceptions | Cannot throw checked exceptions | Can throw checked exceptions |
| Reusability | Low - tightly coupled | High - separates task from thread | High - separates task from execution |
| Use with thread pools | Awkward | Natural fit | Natural fit |
| Recommended | Rarely | General purpose | When result needed |
Thread Lifecycle Management
start()
Transitions thread from NEW to RUNNABLE. Allocates native thread resources.
thread = new Thread(myRunnable)
thread.start() // spawns new OS thread, calls run() in that thread
// WRONG: thread.run() - executes in CURRENT thread, no new thread created
sleep(milliseconds)
Pauses the current thread for a specified duration. Does NOT release locks.
method performWithBackoff(operation, maxRetries):
for attempt in 1..maxRetries:
try:
return operation.execute()
catch TransientError:
backoffMs = 100 * power(2, attempt)
Thread.sleep(backoffMs) // current thread enters TIMED_WAITING
throw MaxRetriesExceeded
join()
Calling thread waits until the target thread terminates.
method processInParallel(tasks):
threads = []
for task in tasks:
t = new Thread(task)
t.start()
threads.add(t)
// Wait for all threads to complete
for t in threads:
t.join() // caller blocks until t finishes
print("All tasks completed")
yield()
Hints to the scheduler that the current thread is willing to give up its time slice. Not guaranteed to have any effect.
method cooperativeLoop():
while not done:
if noWorkAvailable():
Thread.yield() // let other threads run
else:
processNextItem()
interrupt()
Sets the interrupt flag on a target thread. If the thread is sleeping or waiting, it throws InterruptedException.
// Worker thread
method run():
while not Thread.currentThread().isInterrupted():
item = queue.poll(1, SECONDS) // throws InterruptedException
if item != null:
process(item)
// Controller
workerThread.interrupt() // signals the worker to stop
Daemon Threads vs User Threads
| Aspect | User Threads | Daemon Threads |
|---|---|---|
| JVM shutdown | JVM waits for all user threads to finish | JVM exits even if daemon threads are running |
| Default | Yes | No - must explicitly set |
| Use case | Core application logic | Background services (GC, monitoring, logging) |
| Resource cleanup | Guaranteed (finally blocks execute) | NOT guaranteed on JVM exit |
| Creation | Default behavior | thread.setDaemon(true) before start() |
When to Use Each
- User threads: Request processing, transaction handling, any work that must complete
- Daemon threads: Heartbeat senders, cache eviction, log flushing, metrics collection
Pseudocode Example
// Background cache cleanup - daemon thread
class CacheEvictor implements Runnable:
field cache
method run():
while true:
Thread.sleep(30_000) // every 30 seconds
cache.removeExpiredEntries()
log("Evicted stale cache entries")
evictor = new Thread(new CacheEvictor(appCache))
evictor.setDaemon(true) // MUST set before start()
evictor.start()
// Application continues - when main work finishes, JVM exits
// and the evictor is terminated automatically
Thread Priority
Threads can be assigned priority values that hint at scheduling preference:
| Priority | Constant | Typical Use |
|---|---|---|
| 1 | MIN_PRIORITY | Background, low-importance tasks |
| 5 | NORM_PRIORITY | Default for all threads |
| 10 | MAX_PRIORITY | Time-critical operations |
Scheduling Implications
- Priority is a hint, not a guarantee. The OS scheduler makes final decisions.
- Higher-priority threads are more likely to be scheduled first, but starvation of low-priority threads is possible.
- Platform dependency: Windows uses 7 priority levels mapped from 10 Java levels. Linux often ignores thread priorities entirely for non-root processes (nice values apply instead).
Best Practices
- Do not rely on priority for correctness - use synchronization primitives instead.
- Avoid extreme priorities; they cause starvation and priority inversion.
- Use priorities only for performance tuning in well-understood scenarios.
thread = new Thread(criticalTask)
thread.setPriority(Thread.MAX_PRIORITY)
thread.start()
backgroundThread = new Thread(loggingTask)
backgroundThread.setPriority(Thread.MIN_PRIORITY)
backgroundThread.start()
Thread Groups
Thread groups provide hierarchical organization of threads for bulk management.
Organization
// Create a group for all worker threads
workerGroup = new ThreadGroup("download-workers")
for i in 1..5:
t = new Thread(workerGroup, new DownloadTask(urls[i]), "worker-" + i)
t.start()
// Query group
print("Active threads: " + workerGroup.activeCount())
Bulk Operations
// Interrupt all threads in a group
workerGroup.interrupt()
// List all threads
threads = new Thread[workerGroup.activeCount()]
workerGroup.enumerate(threads)
Uncaught Exception Handling
class LoggingThreadGroup extends ThreadGroup:
constructor(name):
super(name)
method uncaughtException(thread, exception):
log.error("Thread " + thread.getName() + " failed: " + exception)
alertOps(thread, exception)
// Optionally restart the thread
Note: Thread groups are considered somewhat legacy. Modern code prefers
ThreadPoolExecutorwith customThreadFactoryandUncaughtExceptionHandler. However, understanding groups is valuable for interview contexts and legacy codebases.
Pseudocode Implementation
Multi-Threaded File Downloader
class DownloadProgress:
field totalFiles
field completedFiles = AtomicInteger(0)
field failedFiles = AtomicInteger(0)
field bytesDownloaded = AtomicLong(0)
constructor(totalFiles):
this.totalFiles = totalFiles
method recordSuccess(bytes):
completedFiles.incrementAndGet()
bytesDownloaded.addAndGet(bytes)
method recordFailure():
failedFiles.incrementAndGet()
method isComplete():
return (completedFiles.get() + failedFiles.get()) >= totalFiles
method report():
print("=== Download Report ===")
print("Total files: " + totalFiles)
print("Completed: " + completedFiles.get())
print("Failed: " + failedFiles.get())
print("Bytes downloaded: " + bytesDownloaded.get())
class DownloadWorker implements Runnable:
field url
field destinationPath
field progress
constructor(url, destinationPath, progress):
this.url = url
this.destinationPath = destinationPath
this.progress = progress
method run():
try:
print("[" + Thread.currentThread().getName() + "] Downloading: " + url)
bytes = httpDownload(url, destinationPath)
progress.recordSuccess(bytes)
print("[" + Thread.currentThread().getName() + "] Completed: " + url)
catch IOException as e:
progress.recordFailure()
print("[" + Thread.currentThread().getName() + "] Failed: " + url + " - " + e.message)
catch InterruptedException:
Thread.currentThread().interrupt() // preserve interrupt status
progress.recordFailure()
class MultiThreadedDownloader:
field maxConcurrentDownloads
constructor(maxConcurrentDownloads):
this.maxConcurrentDownloads = maxConcurrentDownloads
method downloadAll(fileList):
progress = new DownloadProgress(fileList.size())
threads = []
for entry in fileList:
worker = new DownloadWorker(entry.url, entry.dest, progress)
thread = new Thread(worker, "downloader-" + entry.name)
threads.add(thread)
// Start threads in batches to limit concurrency
batches = partition(threads, maxConcurrentDownloads)
for batch in batches:
// Start all threads in this batch
for t in batch:
t.start()
// Join all threads in this batch before starting next
for t in batch:
try:
t.join(30_000) // 30-second timeout per thread
if t.isAlive():
t.interrupt() // force stop if hung
t.join(5_000) // brief wait for cleanup
catch InterruptedException:
print("Main thread interrupted during join")
// Interrupt remaining threads
for remaining in batch:
remaining.interrupt()
break
progress.report()
return progress
// Usage
downloader = new MultiThreadedDownloader(maxConcurrentDownloads: 4)
files = [
{url: "https://cdn.example.com/file1.zip", dest: "/tmp/file1.zip", name: "file1"},
{url: "https://cdn.example.com/file2.zip", dest: "/tmp/file2.zip", name: "file2"},
{url: "https://cdn.example.com/file3.zip", dest: "/tmp/file3.zip", name: "file3"},
// ... more files
]
result = downloader.downloadAll(files)
Common Pitfalls
1. Starting a Thread Twice
thread.start()
thread.start() // throws IllegalThreadStateException!
A thread in TERMINATED state cannot be restarted. Create a new thread instance instead.
2. Not Handling InterruptedException
// WRONG - swallows the interrupt signal
try:
Thread.sleep(1000)
catch InterruptedException:
// empty - other code checking isInterrupted() will miss it
// CORRECT - restore the interrupt flag
try:
Thread.sleep(1000)
catch InterruptedException:
Thread.currentThread().interrupt() // restore flag
return // exit gracefully
3. Thread Leaks
Creating threads without proper lifecycle management leads to resource exhaustion:
// WRONG - unbounded thread creation
method handleRequest(request):
new Thread(() -> process(request)).start() // no limit, no tracking
// CORRECT - use bounded thread pool
executor = new ThreadPoolExecutor(coreSize: 10, maxSize: 50, queue: boundedQueue)
method handleRequest(request):
executor.submit(() -> process(request))
4. Forgetting to Join
// WRONG - main thread may exit before workers finish
for task in tasks:
new Thread(task).start()
print("Done!") // printed before tasks complete
// CORRECT - join all threads
threads = []
for task in tasks:
t = new Thread(task)
t.start()
threads.add(t)
for t in threads:
t.join()
print("Done!") // printed after all tasks complete
5. Calling run() Instead of start()
Calling run() directly executes the method in the current thread - no new thread is created. Always use start().
Real-World Examples
Web Server Request Handling
// Simplified thread-per-request model (e.g., traditional Tomcat)
class WebServer:
field serverSocket
field workerGroup = new ThreadGroup("request-handlers")
method start():
while serverSocket.isOpen():
clientSocket = serverSocket.accept()
handler = new Thread(workerGroup, new RequestHandler(clientSocket))
handler.start()
Background Task Scheduling
// Periodic health check as daemon thread
class HealthChecker implements Runnable:
method run():
while not Thread.currentThread().isInterrupted():
status = checkDatabaseConnection()
status += checkCacheConnection()
publishMetrics(status)
Thread.sleep(10_000)
healthThread = new Thread(new HealthChecker(), "health-checker")
healthThread.setDaemon(true)
healthThread.start()
Periodic Cleanup
// Session cleanup running every 5 minutes
class SessionCleaner implements Runnable:
field sessionStore
method run():
while not Thread.currentThread().isInterrupted():
expired = sessionStore.findExpired()
for session in expired:
sessionStore.remove(session.id)
log("Cleaned " + expired.size() + " sessions")
Thread.sleep(300_000)
Parallel File Processing
// Process log files in parallel, aggregate results
method analyzeLogFiles(directory):
files = listFiles(directory, "*.log")
results = ConcurrentHashMap()
threads = []
for file in files:
t = new Thread(() ->
counts = countErrorsByType(file)
results.put(file.name, counts)
)
t.start()
threads.add(t)
for t in threads:
t.join()
return aggregateResults(results)
Interview Follow-ups
Q1: What happens if you call start() on a thread that has already terminated?
A: It throws IllegalThreadStateException. A thread cannot transition from TERMINATED back to any other state. You must create a new Thread instance to re-execute the same task. This is a deliberate design choice - thread state is tied to OS-level resources that are released upon termination.
Q2: How does sleep() differ from wait()?
A: sleep() pauses the current thread for a fixed duration but does not release any locks it holds. wait() releases the monitor lock on the object and suspends the thread until notify()/notifyAll() is called. sleep() is a static method of Thread; wait() is an instance method of Object and must be called within a synchronized block.
Q3: Why might thread.join(timeout) return with the thread still alive?
A: join(timeout) waits at most the specified milliseconds. If the target thread hasn't terminated within that window, join() returns anyway. The caller should check thread.isAlive() afterward to determine whether the thread actually finished or the timeout elapsed. This is essential for implementing deadlock-free waiting patterns.
Hints-Only Questions
H1: How would you implement a graceful shutdown mechanism for a pool of worker threads?
- Think about a shared volatile
shutdownflag combined withinterrupt() - Consider what happens to in-progress work vs. queued work
H2: What is priority inversion and how can it occur with thread priorities?
- Consider a scenario with three threads at LOW, MEDIUM, and HIGH priority sharing a lock
- Research priority inheritance protocols and how real-time systems solve this
Counter Questions to Ask Interviewer
- "What's the expected concurrency level?" - Determines whether raw threads, thread pools, or reactive patterns are appropriate.
- "Are threads CPU-bound or I/O-bound?" - CPU-bound work benefits from thread count ≈ core count; I/O-bound work can use many more threads.
- "Is there a preference for platform threading vs. virtual threads?" - Modern runtimes (e.g., Java 21+) offer virtual threads that change the cost model entirely.
- "What's the failure tolerance?" - Should a failed thread crash the application, be retried, or be logged and ignored?
- "Are there existing concurrency frameworks in the codebase?" - Avoids reinventing what's already available (Executors, Akka, structured concurrency).
References & Whitepapers
- "Java Concurrency in Practice" - Brian Goetz et al. (2006). The definitive guide to concurrent programming on the JVM.
- "Operating System Concepts" - Silberschatz, Galvin, Gagne. Chapters on process/thread management and scheduling.
- POSIX Threads (pthreads) Specification - IEEE Std 1003.1. Foundation for thread APIs across Unix systems.
- "A Java Fork/Join Framework" - Doug Lea (2000). Describes work-stealing and parallel decomposition.
- JEP 444: Virtual Threads - OpenJDK. Lightweight threads that decouple application concurrency from OS thread count.
- "The Problem with Threads" - Edward A. Lee (2006). UC Berkeley Technical Report on why threads are fundamentally difficult.
- Linux kernel documentation: CFS Scheduler - Explains how Linux schedules threads and why Java priorities have limited effect on Linux.