Introduction
In modern software development, applications are expected to be fast, responsive, and capable of handling multiple tasks simultaneously. Whether it’s a banking system processing thousands of transactions, a web server handling client requests, or a gaming application rendering graphics while processing user input — concurrency and multithreading play a crucial role in making these operations smooth and efficient. Java provides robust support for working with multiple threads, enabling developers to perform parallel tasks, optimize CPU usage, and improve responsiveness.
What is Concurrency?
- Concurrency is the ability of a program to deal with multiple tasks at the same time. It doesn’t always mean they are executed simultaneously, but rather that the program can make progress on multiple tasks without waiting for one to finish completely.
- Example: While downloading a file, your application can also allow the user to type in a search bar.
What is Multithreading?
- Multithreading is a specific form of concurrency where a program is divided into smaller units called threads, which run independently but share the same memory space.
- A thread is the smallest unit of execution in a program. Multiple threads can run in parallel, depending on the number of CPU cores available.
Concurrency vs Multithreading in simple terms:
- Concurrency = Dealing with multiple things at once
- Multithreading = Running multiple threads within the same program
Why Does It Matter?
- In a single-threaded application, tasks are executed one after the other. If one task takes time (like reading a file or fetching data from the network), the whole program is blocked.
- With multithreading, long-running tasks can run in the background while other tasks continue executing, making programs faster and more responsive.
1. What is Multithreading?
Multithreading is the capability of a program to execute multiple threads simultaneously. A thread is the smallest unit of execution within a process. Unlike separate processes, threads in the same program share the same memory space (heap, method area), but each thread maintains its own stack for execution.
In simple terms:
- A process is like an entire program (e.g., Microsoft Word, a Java application).
- A thread is like a worker inside that program doing a specific task (e.g., spell checking, autosaving, responding to user input).
Why Multithreading?
- Responsiveness – Applications don’t freeze while performing heavy tasks. Example: In a chat app, one thread handles UI while another sends/receives messages.
- Better Resource Utilization – Threads share memory and resources, reducing the cost of creating separate processes.
- Scalability – Multithreading allows applications to take advantage of multi-core processors.
- Improved Performance – Tasks like matrix multiplication, web crawling, or handling client requests in a server can be split across threads.
Real-World Examples of Multithreading
- Web Browsers – One thread handles page rendering, another downloads images, and another plays video/audio.
- Video Games – Separate threads for rendering graphics, handling user input, physics calculation, and audio playback.
- Banking System – While one thread processes payments, another handles account updates, and another manages notifications.
- Servers (e.g., Tomcat, Spring Boot) – Multiple threads handle concurrent client requests.
✔️Simple Java Example – Without and With Threads
a. Without Multithreading (Sequential Execution)
public class SingleThreadExample {
public static void main(String[] args) {
task("Task 1");
task("Task 2");
}
public static void task(String name) {
for (int i = 1; i <= 5; i++) {
System.out.println(name + " - step " + i);
}
}
}
Output:
Task 1 - step 1
Task 1 - step 2
...
Task 1 - step 5
Task 2 - step 1
Task 2 - step 2
...
Task 2 - step 5
Here, Task 2 starts only after Task 1 is completely finished.
b. With Multithreading (Concurrent Execution)
public class MultiThreadExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> task("Task 1"));
Thread t2 = new Thread(() -> task("Task 2"));
t1.start();
t2.start();
}
public static void task(String name) {
for (int i = 1; i <= 5; i++) {
System.out.println(name + " - step " + i);
}
}
}
Possible Output (interleaved, concurrent execution):
Task 1 - step 1
Task 2 - step 1
Task 1 - step 2
Task 2 - step 2
Task 2 - step 3
Task 1 - step 3
...
Here, both threads run concurrently, and their steps are interleaved depending on the CPU’s scheduling.
Concurrency vs Parallelism
- Concurrency: Multiple threads make progress at the same time, but not necessarily simultaneously (time-slicing on a single CPU).
- Parallelism: Multiple threads execute at the same time on different CPU cores.
👉 Java’s multithreading model supports both concurrency and parallelism depending on the system hardware.
2. Creating Threads in Java
In Java, there are multiple ways to create and start threads. At the core, every thread in Java needs a piece of code to execute — defined in the run() method. To run that code concurrently, you need to create a Thread object and call its start() method.
Here are the three main approaches:
2.a. Extending Thread Class
The simplest way to create a thread is by extending the built-in Thread class and overriding its run() method. Example:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class ThreadExample1 {
public static void main(String[] args) {
MyThread t1 = new MyThread(); // create a thread instance
t1.start(); // start the thread (calls run() internally)
}
}
Explanation:
run()→ contains the code that will be executed in the new thread.start()→ creates a new thread of execution and then callsrun(). If you directly callrun(), it won’t create a new thread — it will just run in the current thread.
Pros:
- Simple to implement.
- Useful if you don’t need to inherit from another class.
Cons:
- Java doesn’t support multiple inheritance, so if your class already extends another class, you cannot extend
Thread.
2.b. Implementing Runnable Interface
A more flexible approach is to implement the Runnable interface. Here, you define the run() method in a class that implements Runnable and pass it to a Thread object.
Example:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class ThreadExample2 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); // pass Runnable to Thread
t1.start(); // start the thread
}
}
Explanation:
Runnableis a functional interface with a singlerun()method.- You separate the task (Runnable) from the thread management (Thread).
- This approach promotes better code reusability and object-oriented design.
Pros:
- Allows your class to extend another class (since you’re not extending
Thread). - Encourages separation of concerns (task vs execution).
- Preferred in real-world applications.
Cons:
- Slightly more verbose than extending
Thread.
2.c. Using Lambda Expressions (Java 8 and above)
Since Runnable is a functional interface, you can use a lambda expression to create a thread in a more concise way.
Example:
public class ThreadExample3 {
public static void main(String[] args) {
Thread t1 = new Thread(() ->
System.out.println("Thread with Lambda running: " + Thread.currentThread().getName())
);
t1.start();
}
}
Explanation:
- With a lambda, you don’t need to create a separate class or implement
Runnableexplicitly. - This is widely used in modern Java projects because of its readability and conciseness.
Pros:
- Short and clean syntax.
- Great for simple, one-time tasks.
Cons:
- If you have complex business logic, lambdas can make the code less readable compared to a dedicated class.
✍️Which Approach Should You Use?
- Use Extending Thread → when your class doesn’t need to extend anything else and you want a quick implementation.
- Use Implementing Runnable → when your class already extends another class or when you want to separate logic from execution.
- Use Lambda Expressions → when you need quick, inline, and concise thread creation.
3. Thread Lifecycle
In Java, a thread does not simply start and end. It passes through multiple states defined in the Thread.State enum. These states are managed by the Java Virtual Machine (JVM) and the thread scheduler (part of the JVM that decides which thread runs at a given time).
A thread goes through these states:
1. NEW
- A thread is created but not yet started using the
start()method. - Example:
Thread t = new Thread();
2. RUNNABLE
- After
start()is called, the thread is ready to run and waiting for CPU scheduling. - It does not mean the thread is running immediately — only that it’s eligible to run.
3. RUNNING
- When the thread scheduler picks the thread from the runnable pool, it starts execution.
- Only one thread per CPU core can be in the running state at a time.
4. WAITING / BLOCKED / TIMED_WAITING
- WAITING → Thread waits indefinitely for another thread to notify it (using
wait()/notify()). - BLOCKED → Thread is waiting to acquire a lock.
- TIMED_WAITING → Thread is waiting for a specified period (using
sleep(ms),join(ms), orwait(ms)).
5. TERMINATED (Dead)
- Once the thread finishes execution, it enters the terminated state.
- A terminated thread cannot be restarted.

➤ Code Example – Demonstrating Thread States
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("Thread is running...");
try {
Thread.sleep(2000); // moves to TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finished execution");
});
System.out.println("State after creation: " + t1.getState()); // NEW
t1.start();
System.out.println("State after start(): " + t1.getState()); // RUNNABLE
Thread.sleep(500);
System.out.println("State while sleeping: " + t1.getState()); // TIMED_WAITING
t1.join(); // wait for t1 to finish
System.out.println("State after completion: " + t1.getState()); // TERMINATED
}
}
Output:
State after creation: NEW
State after start(): RUNNABLE
Thread is running...
State while sleeping: TIMED_WAITING
Thread finished execution
State after completion: TERMINATED
✍️Key Notes
- The thread scheduler decides which thread runs, and its behavior depends on the JVM and OS.
- Once a thread is terminated, you cannot restart it — you must create a new thread object.
- Understanding thread states is crucial for debugging concurrency issues.
4. Thread Methods
1. start()
- Purpose: Launches a new thread.
- How it works: Moves the thread from the NEW state → RUNNABLE state. The JVM’s thread scheduler then decides when to move it into the RUNNING state.
- Note: You should never call
run()directly because it won’t create a new thread — it will just execute in the current thread.
2. run()
- Purpose: Contains the logic/code that the thread will execute.
- How it works: When
start()is called, the JVM internally invokes therun()method. - Example: In your code, the lambda expression
() -> { ... }is the body of therun()method.
3. sleep(ms)
- Purpose: Temporarily pauses the thread execution for the specified time (in milliseconds).
- How it works: Moves the thread from RUNNING → TIMED_WAITING state.
- After the sleep duration expires, the thread goes back to RUNNABLE.
4. join()
- Purpose: Allows one thread to wait for another thread to finish before proceeding.
- How it works: If the main thread calls
t1.join(), it goes into a WAITING state untilt1finishes execution.
5. isAlive()
- Purpose: Checks if a thread is still active (either RUNNABLE, RUNNING, or WAITING).
- Returns:
true→ thread is alive.false→ thread has finished execution (TERMINATED).
Example
public class ThreadMethods {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000); // Thread goes to TIMED_WAITING
System.out.println("Thread work done");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start(); // Thread moves from NEW → RUNNABLE
t1.join(); // Main thread waits until t1 finishes
System.out.println("Main thread ends");
}
}
Step-by-Step Execution
t1is created → NEW state.t1.start()→ moves to RUNNABLE (waiting for CPU).- Scheduler picks
t1→ RUNNING. - Inside
run(),Thread.sleep(1000)→ goes to TIMED_WAITING for 1 second. - After 1 second, back to RUNNABLE, then RUNNING again → prints
"Thread work done". - Meanwhile,
mainthread callst1.join()→ goes into WAITING untilt1finishes. - Once
t1finishes →mainresumes and prints"Main thread ends".
5. Synchronization
Why synchronization is needed ?
When multiple threads access the same mutable data concurrently, you can get race conditions and data corruption. Many seemingly simple operations (like count++) are actually compound operations — they involve multiple CPU / JVM steps:
count++ expands to:
- Read
countfrom memory into a register. - Add 1.
- Write the new value back to memory.
If two threads do these steps concurrently, their reads/writes can interleave and one update can be lost (a lost update). That’s why we must coordinate access to shared state — i.e., synchronize
📌 What synchronized Does — Real-Life Example
Imagine you and your friend both want to use the same notebook to write.
- If you both write at the same time, the text will overlap and become messy.
- To avoid this, you decide:
👉 Only one person at a time can hold the notebook and write.
👉 The other person has to wait until the notebook is free.
That’s exactly what happens with synchronized in Java:
- Other threads wait outside until it’s free.
- Only one thread at a time can enter the
synchronizedmethod/block.
5.a. Using synchronized keyword
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SyncExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.getCount());
}
}
6. Deadlock
Deadlock happens when two or more threads wait indefinitely for resources locked by each other.
Example
public class DeadlockExample {
public static void main(String[] args) {
final String resource1 = "Resource1";
final String resource2 = "Resource2";
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1 locked resource 1");
try { Thread.sleep(100);} catch (Exception e) {}
synchronized (resource2) {
System.out.println("Thread 1 locked resource 2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2 locked resource 2");
try { Thread.sleep(100);} catch (Exception e) {}
synchronized (resource1) {
System.out.println("Thread 2 locked resource 1");
}
}
});
t1.start();
t2.start();
}
}
7. Executors and Thread Pools
Instead of creating threads manually, Java provides the Executor Framework for better performance and resource management.
Example – Fixed Thread Pool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running in " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
8. Advanced Concurrency Utilities (java.util.concurrent)
- Locks (
ReentrantLock) → More flexible thansynchronized. - CountDownLatch → Wait for multiple threads to finish.
- CyclicBarrier → Wait until a group of threads reach a barrier point.
- Semaphore → Limit number of threads accessing a resource.
- Concurrent Collections →
ConcurrentHashMap,CopyOnWriteArrayList, etc.
Example – CountDownLatch
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
int taskId = i;
new Thread(() -> {
System.out.println("Task " + taskId + " completed");
latch.countDown();
}).start();
}
latch.await(); // Main thread waits
System.out.println("All tasks completed. Main thread continues.");
}
}
Conclusion
Concurrency and multithreading in Java enable developers to build faster, scalable, and more responsive applications. From creating simple threads to using advanced utilities like CountDownLatch and thread pools, Java provides a powerful concurrency toolkit.
Key takeaways:
- Use threads for parallelism and responsiveness.
- Always manage synchronization to avoid race conditions.
- Use Executor Framework instead of manually managing threads.
- Avoid deadlocks by designing lock acquisition carefully.
- Leverage
java.util.concurrentutilities for advanced concurrency management.
