Multithreading Foundations and Thread LifecycleIntroductionModern Java applications frequently perform several activities at once: a web server may receive requests while writing logs, a desktop application may remain responsive while loading data, and a data-processing system may calculate results while reading files. Multithreading provides the programming model needed to structure such applications. A thread is an independently schedulable path of execution within a process. Java threads share the process's heap and other resources, but each thread has its own stack, program counter, and execution state. This shared-memory model enables efficient communication, but it also creates risks such as race conditions, deadlock, and visibility errors. This chapter develops the foundations required for safe and effective multithreaded Java programming. It distinguishes processes from threads, explains concurrency and parallelism, examines the Java thread lifecycle, and introduces Runnable, Callable, interruption, daemon threads, exception handling, and lifecycle management. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish processes, threads, concurrency, and parallelism;
- describe Java thread states and transitions;
- create and start threads using
Runnable and Callable; - explain scheduling and context switching;
- use interruption cooperatively;
- distinguish daemon and user threads;
- handle uncaught exceptions in threads;
- design predictable thread startup, completion, and shutdown;
- identify common multithreading errors and apply professional practices.
Processes, Threads, Concurrency, and ParallelismA process is an executing program with an isolated virtual address space and operating-system resources. Processes are relatively well isolated: one process normally cannot directly access another process's memory. Communication therefore requires mechanisms such as files, sockets, pipes, or operating-system interprocess communication. A thread is a unit of execution inside a process. Threads belonging to one process normally share heap memory, class metadata, open resources, and static variables. Each thread maintains its own call stack, local variables, and program counter. Sharing makes communication inexpensive, but a shared mutable object can be accessed simultaneously by several threads. Concurrency means that multiple tasks make progress during overlapping periods. On a single processor, the operating system can alternate between tasks rapidly, producing the appearance of simultaneous execution. Parallelism means that tasks execute at the same time on different processing cores. A program can be concurrent without being parallel, but parallel execution is also concurrent. A useful distinction is: | Concept | Meaning | Typical benefit |
|---|
| Process | Independent executing program | Isolation and fault containment | | Thread | Execution path within a process | Low-cost task execution and shared data | | Concurrency | Overlapping progress of tasks | Responsiveness and resource utilisation | | Parallelism | Simultaneous execution on multiple cores | Reduced elapsed time for suitable workloads |
Parallelism does not automatically improve performance. Tasks may compete for locks, memory bandwidth, input/output devices, or processor cache. If tasks are too small, the cost of scheduling and context switching can exceed the useful work. The Java Memory and Execution ModelA Java process generally contains a heap shared by its threads. Objects created with new are placed on the heap, while method frames and local variables are associated with individual thread stacks. A reference stored in a shared field may therefore point to an object accessed by many threads. The fact that two threads share an object does not make operations on that object safe. For example, count++ is not one indivisible action. It involves reading count, adding one, and writing the result. Two threads can read the same old value and overwrite one another's updates. Thread safety depends on three related properties: - Atomicity: an operation appears indivisible.
- Visibility: one thread can reliably observe another thread's update.
- Ordering: operations occur in an order consistent with the program's synchronisation rules.
The Java Memory Model defines happens-before relationships. For example, an unlock on a monitor happens-before a later lock on the same monitor; writing to a volatile variable happens-before a subsequent read of that variable; and actions before Thread.start() happen-before actions in the started thread. These rules are essential when reasoning about shared data. This chapter focuses on lifecycle foundations; synchronisation mechanisms are examined in greater depth in later study. Scheduling and Context SwitchingThe operating system or runtime scheduler selects a runnable thread for execution. Scheduling decisions can depend on processor availability, thread priority, fairness, operating-system policy, and blocking events. Java thread priorities are only hints and must not be used to guarantee correctness or ordering. A context switch saves the execution state of one thread and restores the state of another. It allows many threads to share processors, but it has overhead. The cost includes saving registers, changing scheduling data, and potentially losing useful processor-cache contents. Excessive thread creation can therefore reduce performance. Calling start() makes a thread eligible to run; it does not specify the exact time at which its run() method executes. Calling run() directly is an ordinary method call and does not create a new thread. Thread States and LifecycleJava represents the lifecycle through Thread.State values: | State | Meaning |
|---|
NEW | The thread object exists, but start() has not been called. | RUNNABLE | The thread is eligible to run; it may be executing or waiting for processor time. | BLOCKED | The thread is waiting to acquire a monitor lock, usually for a synchronized section. | WAITING | The thread is waiting indefinitely for another action, such as join() without a timeout or Object.wait(). | TIMED_WAITING | The thread is waiting for a bounded period, such as during sleep() or timed join(). | TERMINATED | The thread's run() method has completed or ended because of an uncaught exception. |
RUNNABLE does not mean “currently executing.” Java does not expose a separate official RUNNING state. A runnable thread may be executing on a core or waiting in the scheduler's ready queue.
A thread normally follows this conceptual path: NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → RUNNABLE → TERMINATED
A thread can move from RUNNABLE to a waiting state when it sleeps, waits, joins another thread, or attempts to acquire an unavailable monitor. It returns to RUNNABLE when the relevant condition is satisfied. A thread cannot be restarted after termination; calling start() a second time throws IllegalThreadStateException. The sleep() method pauses the current thread for at least approximately the requested duration, subject to scheduling. It does not release locks held by the thread. By contrast, wait() releases the associated monitor while waiting and must be called with that monitor held. Creating Threads with RunnableThe preferred basic abstraction for a task is often Runnable, whose functional method is: @FunctionalInterface
public interface Runnable {
void run();
}A task describes what should be done, while a Thread describes an execution mechanism. Separating the two improves reuse and makes it easier to submit work to an executor later. public class RunnableExample {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Worker step " + i);
}
};
Thread worker = new Thread(task, "worker-1");
worker.start();
worker.join();
System.out.println("Main method continues after worker completion.");
}
}The call to join() causes the calling thread to wait until worker terminates. The overload join(long millis) waits for at most the specified duration. A timeout is useful when an application must remain responsive, but it does not terminate the target thread. An anonymous class can also implement Runnable, but lambda expressions are concise when the task has one abstract method. Extending Thread is possible, but it couples the task to the thread object and prevents the class from extending another class. For most application tasks, implementing Runnable or using an executor is preferable. Callable and FutureRunnable cannot return a result and cannot declare a checked exception from run(). Callable<V> represents a computation that returns a value of type V and may throw an exception:
public interface Callable<V> {
V call() throws Exception;
}A Callable is normally submitted to an ExecutorService, which returns a Future<V> representing the pending result. import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
try {
Future<Integer> result = pool.submit(() -> {
Thread.sleep(200);
return 6 * 7;
});
System.out.println("Answer: " + result.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("The caller was interrupted.");
} catch (ExecutionException e) {
System.err.println("The task failed: " + e.getCause());
} finally {
pool.shutdown();
}
}
}Future.get() waits until the computation completes. It may throw InterruptedException if the waiting thread is interrupted, or ExecutionException if the task itself terminates with an exception. The original failure is available through getCause().
For production applications, an ExecutorService is generally safer than manually creating an unlimited number of threads. A fixed thread pool bounds the number of simultaneous workers, while a single-thread executor serialises submitted tasks. Executors also provide structured shutdown operations. Interruption: Cooperative CancellationInterruption is Java's standard cooperative cancellation mechanism. Calling target.interrupt() sets the target thread's interrupt status. It does not forcibly stop the thread and does not guarantee immediate termination. Blocking methods such as sleep(), wait(), and join() respond by throwing InterruptedException and clearing the interrupt status. Code that catches the exception should usually restore the status unless it deliberately handles cancellation at that level: try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return; // orderly cancellation
}A CPU-bound task that does not call an interruptible method should periodically test its status: while (!Thread.currentThread().isInterrupted()) {
performSmallUnitOfWork();
}
cleanup();The deprecated Thread.stop() method must not be used because asynchronous termination can leave shared objects in inconsistent states. A well-designed task defines where cancellation is checked, releases resources in finally blocks, and communicates completion to its owner. Daemon and User ThreadsJava distinguishes user threads from daemon threads. The JVM continues running while at least one user thread is alive. It may terminate when no user threads remain, even if daemon threads are still executing. Consequently, daemon threads must not be used for essential work such as committing financial records or completing required file writes. A thread must be marked daemon before it is started: Thread monitor = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
collectMetrics();
try {
Thread.sleep(5_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "metrics-monitor");
monitor.setDaemon(true);
monitor.start();Daemon status is inherited by newly created threads. It is a lifecycle policy, not a substitute for explicit shutdown. Exception Handling in ThreadsAn exception thrown from a thread's run() method does not automatically propagate to the thread that called start(). If it is uncaught, the thread terminates and the JVM invokes its uncaught-exception handling mechanism. A handler can be installed for one thread or as a default handler: Thread worker = new Thread(() -> {
throw new IllegalStateException("Invalid input");
}, "validated-worker");
worker.setUncaughtExceptionHandler((thread, error) ->
System.err.println(thread.getName() + " failed: " + error));
worker.start();Handlers should record useful diagnostic information and alert an appropriate supervisory component. They should not assume that the failed thread can continue. With Future, task exceptions are normally captured and reported through ExecutionException when the result is retrieved. Avoid empty catch blocks. At minimum, preserve the cause, provide context, and decide whether the application should retry, cancel related work, or terminate safely. Thread Lifecycle Management and Best PracticesProfessional thread management includes ownership, naming, bounded resources, cancellation, and completion policies. - Give threads meaningful names. Names such as
invoice-worker-2 improve logs and debugging. - Prefer tasks and executors. Separate work from the mechanism that runs it.
- Bound concurrency. A pool size should reflect workload, available processors, blocking behaviour, and external capacity.
- Always define shutdown behaviour. Call
shutdown() to reject new tasks and allow submitted tasks to finish; use shutdownNow() as a cooperative interruption request when an urgent stop is required. - Use
awaitTermination() where appropriate. It allows a coordinating thread to wait for orderly termination with a deadline. - Do not rely on timing for correctness.
sleep() is not a synchronisation technique. - Protect shared mutable state. Use appropriate synchronisation, concurrent collections, immutable objects, or message-passing designs.
- Handle interruption correctly. Restore the status or complete cancellation handling.
- Release resources in
finally or try-with-resources. Thread termination must not leak files, connections, or locks. - Avoid unbounded queues and thread creation. They can cause memory exhaustion and make failure difficult to control.
A thread dump is a valuable diagnostic tool. It can reveal blocked threads, waiting threads, deadlocks, and unexpectedly long-running tasks. Monitoring should include active-task counts, queue length, task latency, rejected tasks, and failure rates. Common Errors and LimitationsCalling run() instead of start()Calling run() performs the task on the current thread. No concurrency is created. Assuming output orderEven if two threads are started in a particular order, their output order is not guaranteed. Use explicit coordination when ordering matters. Ignoring visibilityA loop that reads an ordinary shared boolean may not reliably observe a change made by another thread. Use a suitable visibility or synchronisation mechanism, such as volatile, a lock, or a higher-level concurrency utility. Race conditionsA race occurs when the result depends on unpredictable timing between threads. Compound operations require protection even when individual reads and writes appear simple. DeadlockDeadlock occurs when threads wait indefinitely for locks held by one another. Consistent lock ordering, small critical sections, time-limited acquisition, and higher-level utilities reduce the risk. Starvation and livelockStarvation occurs when a thread rarely receives required resources. Livelock occurs when threads remain active but repeatedly respond to one another without making progress. Fairness, bounded retries, and back-off policies may help. Resource contentionMore threads do not necessarily mean more throughput. Database connection limits, disk bandwidth, remote service quotas, and CPU cores impose practical boundaries. Illustrative Case: Order Processing ServiceBackgroundConsider an online retailer that receives orders through a Java service. Each order must be validated, charged, recorded, and followed by a confirmation message. ProblemAn initial implementation creates a new thread for every order and uses a shared integer counter for successful orders. During a traffic surge, memory usage rises, the database becomes overloaded, and the counter is lower than the number of successful transactions. AnalysisCreating an unlimited number of threads increases scheduling and stack-memory overhead. The counter update is a compound operation and is therefore vulnerable to lost updates. In addition, abruptly stopping threads could leave database transactions incomplete. Approach and SolutionThe service uses a bounded ExecutorService for order tasks. Each task is represented as a Callable<OrderResult>, allowing success or failure to be reported through a Future. Database operations use transaction boundaries and try-with-resources. Shared statistics are updated through a suitable atomic or synchronised mechanism. During shutdown, the service stops accepting new orders, calls shutdown(), waits for existing tasks, and then uses a controlled cancellation policy for tasks that exceed the deadline. A separate daemon monitoring thread reports queue length and latency, but it is not responsible for committing orders. Interruption is treated as a cancellation request, and the task restores interrupt status after catching InterruptedException. OutcomeThe bounded pool prevents unlimited thread growth, database load becomes measurable and controllable, failures are associated with individual orders, and shutdown becomes predictable. The system still requires careful capacity testing: a pool that is too large can overload the database, while one that is too small can increase waiting time. Learning PointsThe case demonstrates that multithreading is a resource-management problem as well as a programming technique. Correct lifecycle management, bounded concurrency, safe shared state, and cooperative cancellation are all necessary for a reliable service. Contemporary Java PracticeModern Java development increasingly uses ExecutorService, Future, CompletableFuture, concurrent collections, and structured task-management patterns rather than directly managing individual threads. Newer Java releases also provide virtual threads, which are lightweight threads suitable for many blocking, input/output-oriented tasks. Virtual threads reduce the cost of representing concurrent tasks, but they do not eliminate database limits, race conditions, deadlocks, or the need for cancellation and error handling. CPU-bound work still depends on available processor capacity. Concurrency also has governance implications. Applications must protect personal and financial data, avoid logging sensitive information from worker failures, and ensure that cancellation does not bypass audit or transaction requirements. Operational policies should define timeouts, retry limits, escalation, and shutdown procedures. Chapter SummaryA process is an isolated executing program, whereas a thread is an execution path within a process. Threads enable concurrency and may provide parallelism on multicore systems, but shared memory introduces visibility, atomicity, and ordering concerns. Java thread states include NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. start() creates asynchronous execution; run() alone does not. Runnable represents a task without a return value, while Callable supports results and exceptions, commonly accessed through Future. Interruption is cooperative and should be handled by restoring interrupt status or completing a clear cancellation path. Daemon threads are suitable for non-essential background activity only. Uncaught exceptions terminate their thread unless handled, and executor-based designs usually provide better control than unlimited manual thread creation.
Reliable multithreaded systems use bounded resources, meaningful names, explicit shutdown, safe shared-state techniques, timeout policies, and monitoring. Correctness must never depend on accidental scheduling order. Key TermsProcess: An executing program with its own protected address space and resources. Thread: An independently schedulable execution path within a process. Concurrency: Overlapping progress of multiple tasks. Parallelism: Simultaneous execution of tasks on multiple processing cores. Scheduler: A system component that selects eligible threads for execution. Context switch: Saving one execution context and restoring another. Runnable: A task abstraction with a run() method and no result. Callable: A task abstraction that returns a value and may throw an exception. Future: A handle representing a pending or completed asynchronous result. Interruption: A cooperative request that a thread stop or reconsider its work. Daemon thread: A background thread that does not keep the JVM alive by itself. User thread: A non-daemon thread whose existence keeps the JVM running. join(): An operation that waits for another thread to terminate.
Race condition: Incorrect behaviour caused by timing-dependent concurrent access. Visibility: The ability of one thread to observe another thread's updates. Atomicity: The property that an operation appears indivisible. Deadlock: Indefinite waiting caused by cyclic resource dependencies. Starvation: Failure of a thread to obtain resources or execution time. ExecutorService: An API for managing task execution and worker resources. Thread lifecycle: The sequence of states and transitions experienced by a thread. Review QuestionsShort Answer Questions- What is the difference between a process and a thread?
- Why does calling
run() directly not create a new thread? - What does the Java
RUNNABLE state mean? - Why should a caught
InterruptedException often be followed by Thread.currentThread().interrupt()? - Why are daemon threads unsuitable for essential data-processing work?
Long Answer Questions- Explain Java's thread states and describe the events that cause transitions between them.
- Compare
Runnable and Callable, including how results and exceptions are handled. - Explain scheduling and context switching and discuss their effect on performance.
- Describe a professional shutdown strategy for an
ExecutorService. - Discuss race conditions, visibility, deadlock, starvation, and livelock as multithreading risks.
Analytical / Application Questions- A program starts two threads and expects the first-started thread to print its message first. Explain why this expectation is invalid and propose a correct coordination approach.
- An order service creates one thread per request and becomes unresponsive during peak traffic. Analyse the likely causes and design a bounded, cancellable replacement.
- A worker catches
InterruptedException, logs it, and continues its infinite loop without restoring the interrupt status. Evaluate the defect and write a corrected handling strategy.
Multiple Choice Questions with AnswersQuestion 1Which statement best distinguishes concurrency from parallelism? A. Concurrency requires multiple processors, while parallelism requires one. B. Concurrency involves overlapping progress, while parallelism involves simultaneous execution. C. Concurrency applies only to processes, while parallelism applies only to threads. D. They are two names for exactly the same concept. Correct Answer: B. Concurrency involves overlapping progress, while parallelism involves simultaneous execution. Question 2What happens when run() is called directly on a Thread object? A. A new operating-system process is created. B. The method executes on the current thread like an ordinary method call. C. The thread enters WAITING state. D. The JVM automatically creates a thread pool. Correct Answer: B. The method executes on the current thread like an ordinary method call. Question 3Which Java state indicates that a thread is waiting to acquire a monitor lock? A. NEW B. WAITING C. BLOCKED D. TERMINATED Correct Answer: C. BLOCKED Question 4Which interface is appropriate for a task that returns a result and may throw an exception? A. Runnable B. Serializable C. Callable D. Comparator Correct Answer: C. Callable Question 5What is the normal effect of calling interrupt() on a thread? A. It forcibly destroys the thread immediately. B. It requests cooperative cancellation by setting interrupt status or interrupting a blocking operation. C. It releases every lock held by the thread. D. It restarts a terminated thread. Correct Answer: B. It requests cooperative cancellation by setting interrupt status or interrupting a blocking operation. Question 6What should generally be done after catching InterruptedException when the current layer is not completing cancellation? A. Ignore the exception permanently. B. Call Thread.currentThread().interrupt(). C. Call Thread.stop(). D. Convert the thread into a daemon. Correct Answer: B. Call Thread.currentThread().interrupt(). Question 7Why should essential file or transaction work not be assigned only to daemon threads? A. Daemon threads cannot perform input/output. B. Daemon threads always run at lower processor speed. C. The JVM may exit when no user threads remain, even if daemon threads are active. D. Daemon threads cannot catch exceptions. Correct Answer: C. The JVM may exit when no user threads remain, even if daemon threads are active. Question 8What does join() normally do? A. It merges two processes permanently. B. It causes the calling thread to wait for the target thread to terminate. C. It makes a thread daemon. D. It cancels all tasks in an executor. Correct Answer: B. It causes the calling thread to wait for the target thread to terminate. Question 9How is an exception from a manually created thread's run() method normally handled if uncaught? A. It automatically propagates to the caller of start(). B. It is ignored and the thread continues. C. The thread terminates and the uncaught-exception mechanism is invoked. D. The JVM always terminates immediately. Correct Answer: C. The thread terminates and the uncaught-exception mechanism is invoked. Question 10Which practice best limits resource exhaustion in a busy Java service? A. Create an unlimited number of threads. B. Use sleep() as the only synchronisation method. C. Use a bounded executor and define an explicit shutdown policy. D. Depend on thread priority for correctness. Correct Answer: C. Use a bounded executor and define an explicit shutdown policy.
|