Java Application
|
+── Thread 1 → Handle Request
|
+── Thread 2 → Process File
|
+── Thread 3 → Perform Background TaskJava Multithreading: Concurrency Fundamentals, Threads, and Lifecycle
Direct Answer & Core Concept
Java multithreading allows a program to perform multiple tasks concurrently. Instead of making a program wait for one task to completely finish before starting another, different threads can make progress independently.
Multithreading is useful for applications that perform background processing, network operations, file handling, user-interface work, server requests, and other tasks that can benefit from concurrency.
To understand Java concurrency properly, you first need to understand threads, processes, thread creation, thread states, lifecycle, scheduling, and basic thread methods.
Detailed Explanation & Mechanics
What Is Multithreading in Java?
Multithreading is the execution of multiple threads within a single Java application.
A thread is a lightweight unit of execution.
For example, a web application might have different work happening at the same time:
These threads share the resources of the same process while having their own execution path.
What Is a Thread?
A thread is an independent path of execution within a program.
Every Java application starts with at least one thread: the main thread.
Consider:
public class Main {
public static void main(String[] args) {
System.out.println("Hello Java");
}
}The main() method runs on the main thread.
Conceptually:
Java Program
|
└── Main Thread
|
└── main()You can create additional threads when your application needs concurrent work.
Process vs Thread
A process is a running program with its own operating-system resources.
A thread is an execution path within a process.
A simple comparison:
| Process | Thread |
|---|---|
| Larger unit of execution | Smaller unit of execution |
| Has its own process resources | Shares many process resources |
| More expensive to create | Generally cheaper to create |
| Processes are isolated from each other | Threads within a process share memory |
| Can contain multiple threads | Belongs to a process |
For example:
Application Process
|
+── Thread 1
+── Thread 2
+── Thread 3Concurrency vs Parallelism
These two terms are related but not identical.
Concurrency
Concurrency means multiple tasks can make progress during overlapping periods.
For example:
Task A: ███ ███Task B: ███ ███The tasks may take turns.
Parallelism
Parallelism means multiple tasks are actually executing at the same time, typically on different CPU cores.
Core 1 → Task ACore 2 → Task BTherefore:
Concurrency is about dealing with multiple tasks; parallelism is about executing multiple tasks simultaneously.
A multithreaded Java program may be concurrent, parallel, or both depending on the runtime environment and hardware.
Why Use Multithreading?
Multithreading can be useful when an application has independent or blocking tasks.
Common examples include:
web servers
background jobs
file processing
network requests
database operations
downloading files
scheduled tasks
GUI applications
simulations
CPU-intensive calculations
For example, a program might download several independent files concurrently:
Main Program
|
+── Thread 1 → Download File A
+── Thread 2 → Download File B
+── Thread 3 → Download File CCreating a Thread in Java
There are several ways to perform work concurrently in Java.
The classic approaches include:
Extending
ThreadImplementing
Runnable
Modern Java applications also commonly use:
ExecutorServiceCallableandFutureHigher-level concurrency utilities
For understanding the fundamentals, start with Thread and Runnable.
Creating a Thread by Extending Thread
You can create a thread by extending the Thread class.
class MyThread extends Thread {
@Override public void run() {
System.out.println("Thread is running");
}
}Then create the thread:
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}The important method here is:
run()The run() method contains the work performed by the thread.
Why Use start() Instead of run()?
This is one of the most important beginner concepts.
Correct:
thread.start();Not:
thread.run();Calling:
thread.start();asks the JVM to start a new thread of execution.
Calling:
thread.run();is simply a normal method call executed by the current thread.
For example:
MyThread thread = new MyThread();
thread.run();does not create a new thread.
The distinction is:
start()↓New thread execution↓run()Whereas:
run()↓Normal method callCreating a Thread with Runnable
A more flexible traditional approach is to implement Runnable.
class MyTask implements Runnable {
@Override public void run() {
System.out.println("Task is running");
}
}Then:
public class Main {
public static void main(String[] args) {
Runnable task = new MyTask();
Thread thread = new Thread(task);
thread.start();
}
}Here:
Runnable↓Defines the taskThread↓Runs the taskThis separation between task and thread is often preferable to extending Thread.
Runnable with Lambda Expression
Because Runnable is a functional interface, you can use a lambda expression.
public class Main {
public static void main(String[] args) {
Runnable task =() -> {
System.out.println("Task is running");
};
Thread thread = new Thread(task);
thread.start();
}
}This is a concise modern Java style.
You can also write:
Thread thread = new Thread(() -> System.out.println("Task is running"));
thread.start();Running Multiple Threads
You can create multiple threads.
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> System.out.println("Task 1"));
Thread thread2 = new Thread(() -> System.out.println("Task 2"));
thread1.start();
thread2.start();
}
}The output order is not guaranteed.
You might see:
Task 1Task 2or:
Task 2Task 1The JVM and operating system determine how threads are scheduled.
Thread Scheduling
When multiple threads are ready to run, the operating system and JVM work together to determine when each thread gets CPU time.
For example:
Time →Thread A: ███ ███Thread B: ███ ███You should not generally write code that depends on an exact scheduling order unless you explicitly establish that ordering through appropriate synchronization or coordination mechanisms.
This is one reason concurrent programs can produce different output between runs.
Thread Lifecycle
A Java thread moves through several states during its lifetime.
Java represents these states using:
Thread.StateThe official states are:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATEDA simplified lifecycle looks like:
NEW | start()↓RUNNABLE / \ / \BLOCKED WAITING | | | | +----→ RUNNABLE |↓TERMINATEDThe exact transitions depend on what the thread is doing.
NEW State
A thread is in the NEW state after the Thread object has been created but before start() has been called.
Example:
Thread thread = new Thread(() -> {
System.out.println("Running");
});At this point:
Thread.State.NEWThe thread has not started execution yet.
RUNNABLE State
After calling:
thread.start();the thread becomes eligible to run.
Its Java thread state is:
RUNNABLERUNNABLE represents a thread that is ready to run or is actually running; Java's state model does not have a separate RUNNING state.
The operating system scheduler determines when it actually gets CPU time.
BLOCKED State
A thread enters BLOCKED when it is waiting to acquire a monitor lock required to enter a synchronized block or method.
For example:
synchronized (lock) {
// protected work
}If another thread already owns the same monitor, a competing thread may become:
BLOCKEDThis state is mainly relevant when studying synchronization.
WAITING State
A thread is in WAITING when it is waiting indefinitely for another thread or action to wake it.
Examples include:
Object.wait()and:
Thread.join()without a timeout.
A waiting thread remains waiting until an appropriate event occurs.
TIMED_WAITING State
A thread enters TIMED_WAITING when it waits for a specified period.
Examples include:
Thread.sleep(1000);and:
thread.join(1000);The thread waits for a limited amount of time.
TERMINATED State
A thread reaches TERMINATED after its execution has completed.
For example:
Thread thread = new Thread(() -> {
System.out.println("Work completed");
});
thread.start();After the run() method finishes, the thread terminates.
A terminated thread cannot be started again.
Calling:
thread.start();a second time results in:
IllegalThreadStateExceptionChecking a Thread's State
You can use:
getState()Example:
Thread thread = new Thread(() -> {
System.out.println("Running");
});
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());The exact second state depends on timing. It may already have completed by the time you check it.
This is an important lesson:
Thread state can change quickly, so observations made from another thread are inherently timing-dependent.
Thread Names
Giving threads meaningful names can make debugging easier.
You can set a name:
Thread thread = new Thread(() -> System.out.println("Working"));
thread.setName("FileProcessor");
thread.start();Or specify the name during construction:
Thread thread = new Thread(() -> System.out.println("Working"), "FileProcessor");You can retrieve the name using:
System.out.println(thread.getName());currentThread()
Java provides:
Thread.currentThread()to obtain the thread currently executing the code.
Example:
public class Main {
public static void main(String[] args) {
System.out.println( Thread.currentThread().getName());
}
}Typical output:
mainThis is useful for debugging and understanding which thread is executing a piece of code.
Thread ID
You can obtain a thread's ID:
long id = thread.getId();Example:
Thread thread = new Thread(() -> {
System.out.println("Running");
});
System.out.println(thread.getId());Thread IDs are assigned by the JVM and are useful for diagnostics.
Thread Sleep
Thread.sleep() pauses the current thread for approximately the specified duration.
Example:
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("Start");
Thread.sleep(1000);
System.out.println("After one second");
}
}The thread enters TIMED_WAITING during the sleep.
sleep() Does Not Release Locks
This is an important distinction.
If a thread is inside a synchronized block and calls:
Thread.sleep(1000);it does not automatically release the monitor lock it holds.
For example:
synchronized (lock) {
Thread.sleep(1000);
}The thread keeps the lock while sleeping.
This differs from coordination mechanisms such as Object.wait(), which releases the associated monitor while waiting.
Interrupting a Thread
Java provides interruption as a cooperative mechanism for requesting that a thread stop what it is doing or respond to cancellation.
You can request interruption using:
thread.interrupt();Example:
Thread worker = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
Thread.currentThread().interrupt();
}
});
worker.start();
worker.interrupt();An interrupt is not a forced termination mechanism.
The interrupted thread should respond appropriately.
What Is InterruptedException?
Some blocking methods respond to interruption by throwing:
InterruptedExceptionFor example:
Thread.sleep(5000);can throw InterruptedException.
A common practice when you do not have another appropriate recovery strategy is to restore the interrupt status:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}This preserves the interruption signal for higher-level code.
Joining Threads
Sometimes one thread needs to wait for another thread to finish.
Use:
join()Example:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker running");
});
worker.start();
worker.join();
System.out.println("Worker finished");
}
}The main thread waits for worker to terminate before continuing past join().
join() with Timeout
You can also specify a maximum waiting time:
worker.join(1000);The current thread waits for up to approximately 1000 milliseconds.
If the worker finishes earlier, the wait can end earlier.
Thread Priority
Java threads have priorities.
You can set a priority using:
thread.setPriority(Thread.MAX_PRIORITY);Java defines constants such as:
Thread.MIN_PRIORITYThread.NORM_PRIORITYThread.MAX_PRIORITYHowever, thread priority should not be used as a reliable mechanism for application correctness or precise scheduling.
Operating-system scheduling and runtime behavior can vary.
Use explicit coordination when correctness depends on ordering.
Daemon Threads
A thread can be marked as a daemon thread:
thread.setDaemon(true);This must be done before:
thread.start();Daemon threads are background threads associated with the JVM's lifecycle.
When no live non-daemon threads remain, the JVM can terminate without waiting for daemon threads to finish.
Therefore, daemon threads should not be used for critical work that must complete before application shutdown.
Example of a Daemon Thread
public class Main {
public static void main(String[] args) {
Thread background = new Thread(() -> {
while (true) {
System.out.println("Background task");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
background.setDaemon(true);
background.start();
System.out.println("Main thread finished");
}
}The JVM may exit once the main thread and other non-daemon threads have completed.
Thread Lifecycle Example
The following example demonstrates a simple lifecycle:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println(worker.getState());
worker.start();
System.out.println(worker.getState());
worker.join();
System.out.println(worker.getState());
}
}A typical sequence is:
NEWRUNNABLETERMINATEDThe exact intermediate state can vary because thread scheduling is nondeterministic.
Thread Lifecycle Diagram
A useful mental model is:
create Thread
|
↓
NEW
|
start()
|
↓
RUNNABLE
/ | \
/ | \
↓
↓
↓
BLOCKED WAITING TIMED_WAITING
\ | /
\ | /
└──→ RUNNABLE
|
|
run() ends
|
↓
TERMINATEDNot every thread visits every state.
The transitions depend on what operations the thread performs.
Multiple Threads Example
Consider a program with two worker threads:
public class Main {
public static void main(String[] args) {
Thread task1 = new Thread(() -> {
for (int i = 1;
i <= 5;
i++) {
System.out.println("Task 1: " + i);
}
});
Thread task2 = new Thread(() -> {
for (int i = 1;
i <= 5;
i++) {
System.out.println("Task 2: " + i);
}
});
task1.start();
task2.start();
}
}The output may interleave:
Task 1: 1Task 2: 1Task 2: 2Task 1: 2Task 1: 3Task 2: 3...The exact order should not be assumed.
Why Output Order Can Change
When multiple threads execute concurrently, the scheduler can switch between them.
Therefore, this:
thread1.start();
thread2.start();does not mean:
Thread 1 completely finishes↓Thread 2 startsInstead, both threads become eligible to execute.
Thread 1 ──┐
├── CPU scheduling
Thread 2 ──┘If the program requires a particular order, use explicit coordination such as join(), locks, conditions, or higher-level concurrency utilities.
Thread Safety
Multithreading becomes more complicated when multiple threads access the same mutable data.
For example:
int count = 0;If several threads update count concurrently:
count++;the result may not be what you expect.
The operation involves multiple conceptual steps:
Read count↓Add 1↓Write countAnother thread can interfere between these steps.
This leads to the broader topic of race conditions and synchronization.
Shared Data Example
Suppose two threads access the same object:
class Counter {
int count = 0;
void increment() {
count++;
}
}If multiple threads call:
counter.increment();at the same time, the operation is not automatically safe merely because it looks like one line of Java code.
This is why Java provides synchronization and concurrency utilities.
Multithreading and Memory
Threads within the same Java process can share objects stored in the common application memory.
Conceptually:
Process
|
+── Thread 1 ──┐
| |
+── Thread 2 ──┼── Shared Objects
| |
+── Thread 3 ──┘This shared-memory model makes communication between threads efficient, but it also creates risks when mutable state is accessed without appropriate coordination.
Common Thread Mistakes
Mistake 1: Calling run() Instead of start()
Wrong for starting a new thread:
thread.run();Correct:
thread.start();Mistake 2: Starting a Thread Twice
This is invalid:
thread.start();
thread.start();A Java Thread instance can be started only once.
Mistake 3: Assuming Thread Order
Do not assume:
thread1.start();
thread2.start();means thread 1 always executes first.
Scheduling is not guaranteed that way.
Mistake 4: Using sleep() for Synchronization
This is fragile:
Thread.sleep(1000);Using a fixed delay does not reliably guarantee that another task has completed.
Prefer explicit coordination such as:
thread.join();or suitable concurrency mechanisms.
Mistake 5: Ignoring InterruptedException
Avoid silently swallowing interruption:
catch (InterruptedException e) {
}When appropriate, preserve the interrupt:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Mistake 6: Assuming Multithreading Automatically Makes Programs Faster
More threads do not always mean better performance.
Too many threads can introduce:
scheduling overhead
context switching
contention
synchronization overhead
increased memory usage
The appropriate concurrency design depends on the workload.
Thread vs Runnable
| Feature | Extending Thread | Implementing Runnable |
|---|---|---|
| Defines task | Yes | Yes |
| Extends a class | Yes | No |
| Separates task from thread | Less | Better |
| Can extend another class | No | Yes |
| Common traditional approach | Yes | Yes |
| Flexible design | Lower | Higher |
For many traditional examples, Runnable is a better separation of concerns.
Modern Java: ExecutorService
Creating raw threads manually is useful for learning, but production applications often use higher-level concurrency abstractions.
For example:
ExecutorService executor = Executors.newFixedThreadPool(3);Then submit tasks:
executor.submit(() -> System.out.println("Task running"));Finally:
executor.shutdown();An executor manages a pool of worker threads and lets your application submit tasks without manually creating a new Thread for every job.
This is a major topic in Java concurrency and should be learned after understanding the basic thread model.
Why Thread Pools Are Useful
Imagine processing 10,000 tasks.
Creating 10,000 independent threads is generally not a good design.
A thread pool can instead maintain a controlled number of worker threads:
Task Queue
|
+── Task 1
+── Task 2
+── Task 3
+── Task 4
+── ...
|
↓
Thread Pool
┌───────────┐
│ Worker 1 │
│ Worker 2 │
│ Worker 3 │
└───────────┘Tasks can be executed by available workers.
Virtual Threads
Modern Java also provides virtual threads, which are lightweight threads managed by the Java runtime.
They are especially useful for applications with large numbers of tasks that spend significant time waiting on blocking operations.
A basic example in modern Java is:
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread");
});Virtual threads do not replace every use case for platform threads. CPU-intensive work still depends on available CPU resources, while virtual threads are particularly valuable for high-concurrency workloads involving blocking operations.
Platform Threads vs Virtual Threads
A simplified comparison:
| Platform Threads | Virtual Threads |
|---|---|
| Backed by OS threads | Managed by the Java runtime |
| More resource-intensive | Much lighter |
| Suitable for many traditional workloads | Excellent for high-concurrency blocking workloads |
| Limited by OS-thread resources | Can support very large numbers of concurrent tasks |
Virtual threads are an important modern Java concurrency feature.
Real-World Multithreading Examples
Web Server
A server may process multiple client requests concurrently:
Request 1 → Thread ARequest 2 → Thread BRequest 3 → Thread CFile Processing
Different independent files can be processed concurrently:
File A → Worker 1File B → Worker 2File C → Worker 3Background Processing
A web application can perform background work without blocking the main request-handling path.
Network Operations
Multiple independent network operations can overlap while waiting for responses.
Data Processing
Large workloads can sometimes be divided into independent tasks.
A Simple Real-World Example
Imagine an online shopping application.
A request might involve:
Order Request
|
+── Validate Order
|
+── Update Inventory
|
+── Send Notification
|
+── Record AnalyticsSome operations may be suitable for asynchronous or concurrent processing depending on application requirements.
The goal is not simply to create more threads. The goal is to design tasks so that concurrency improves responsiveness, throughput, or resource utilization without creating correctness problems.
Best Practices for Java Multithreading
1. Prefer tasks over manually managing many threads
Use Runnable, Callable, executor services, or higher-level concurrency APIs where appropriate.
2. Keep shared mutable state limited
The less shared mutable state you have, the fewer synchronization problems you usually face.
3. Do not depend on accidental thread ordering
If order matters, establish it explicitly.
4. Handle interruption properly
Treat interruption as a cooperative cancellation signal.
5. Avoid unnecessary thread creation
Thread pools and virtual threads can be more appropriate depending on the workload.
6. Use synchronization deliberately
Synchronization is important when shared state requires coordination, but excessive locking can reduce scalability.
7. Prefer higher-level concurrency utilities
Java provides tools such as:
ExecutorServiceFutureCompletableFutureConcurrentHashMapBlockingQueueCountDownLatchSemaphoreThese can solve common concurrency problems more safely than manually coordinating raw threads.
Complete Multithreading Example
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker1 = new Thread(() -> {
for (int i = 1;
i <= 5;
i++) {
System.out.println( Thread.currentThread().getName() + " → " + i);
}
}, "Worker-1");
Thread worker2 = new Thread(() -> {
for (int i = 1;
i <= 5;
i++) {
System.out.println( Thread.currentThread().getName() + " → " + i);
}
}, "Worker-2");
worker1.start();
worker2.start();
worker1.join();
worker2.join();
System.out.println("All work completed");
}
}Possible output:
Worker-1 → 1Worker-2 → 1Worker-1 → 2Worker-2 → 2Worker-2 → 3Worker-1 → 3...All work completedThe exact order of the worker output can vary.
The important guarantee here is that:
worker1.join();
worker2.join();causes the main thread to wait until both worker threads have completed before printing:
All work completedFrequently Asked Questions
What is multithreading in Java?
Multithreading is the execution of multiple threads within a Java process, allowing different tasks to make progress concurrently.
What is a thread?
A thread is an independent path of execution within a process.
What is the main thread?
The main thread is the thread that begins execution of a Java application's main() method.
How do you create a thread in Java?
Traditional approaches include extending Thread or implementing Runnable. Modern applications also commonly use executor frameworks and virtual threads.
What is the difference between start() and run()?
start() initiates a new thread of execution. Calling run() directly is just a normal method call on the current thread.
Can a Java thread be started twice?
No. A Thread object can be started only once.
What are the Java thread states?
The official states are:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATEDWhat does sleep() do?
Thread.sleep() pauses the current thread for approximately the specified duration.
Does sleep() release a lock?
No. Sleeping does not automatically release monitors held by the thread.
What does join() do?
join() causes the current thread to wait for another thread to terminate.
What is a daemon thread?
A daemon thread is a background thread that does not keep the JVM alive by itself after all non-daemon threads have terminated.
What is ConcurrentModificationException?
It is commonly associated with fail-fast detection of certain structural modifications made to a collection while it is being iterated.
Does multithreading always improve performance?
No. Thread creation, scheduling, synchronization, contention, and context switching can introduce overhead.
What is a virtual thread?
A virtual thread is a lightweight Java thread managed by the Java runtime and designed to support very high levels of concurrency, particularly for blocking workloads.
Graphics and Design Ideas
Graphic 1: Java Thread Lifecycle
Create a clean lifecycle diagram:
┌──────────┐
│ NEW │
└────┬─────┘
│ start()
↓
┌───────────┐
│ RUNNABLE │
└─────┬─────┘
↙
↓
↘
BLOCKED WAITING TIMED
↘
↓
↙
┌───────────┐
│ RUNNABLE │
└─────┬─────┘
│
↓
┌─────────────┐
│ TERMINATED │
└─────────────┘This should be the main visual for the page.
Graphic 2: Process vs Threads
Java Process
|
┌──────────┼──────────┐
↓
↓
↓
Thread 1 Thread 2 Thread 3
| | |
Task Task TaskUse different icons for the process and individual threads.
Graphic 3: start() vs run()
thread.start()↓New Thread↓run()↓Task executesCompared with:
thread.run()↓Normal method call↓Current thread executesGraphic 4: Concurrency vs Parallelism
ConcurrencyCPU↓A → B → A → BParallelismCPU 1 → Task ACPU 2 → Task BThis visual can make the distinction easy for beginners.
Graphic 5: Thread Pool
Task Queue
┌──┬──┬──┬──┐
↓
↓
↓
↓
↓
┌──────────────────┐
│ Thread Pool │
│ Worker 1 │
│ Worker 2 │
│ Worker 3 │
└──────────────────┘This provides a natural introduction to the next topic, ExecutorService.
Runnable Code Example: Java Multithreading
Java 17+public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(200);
System.out.println("Worker completed task on thread: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("State before start: " + worker.getState());
// NEW
worker.start();
System.out.println("State after start: " + worker.getState());
// RUNNABLE
worker.join();
System.out.println("State after completion: " + worker.getState());
// TERMINATED
}
}