In today’s world of high-performance computing and responsive applications, multithreading is an essential programming concept that allows programs to perform multiple tasks simultaneously. Java, being a powerful and versatile language, provides robust support for multithreading.
In this guide, we’ll explore what multithreading is, why it matters, and how you can implement it effectively in Java.
What is Multithreading?
Multithreading is a feature of a program that enables multiple threads of execution to run concurrently within a single process. A thread is the smallest unit of execution in a program. Imagine a program as a busy kitchen where multiple chefs (threads) work simultaneously on different dishes (tasks) — this speeds up the overall cooking process.
Why Use Multithreading?
- Improved Performance: By running tasks concurrently, your program can complete jobs faster, especially on multi-core processors.
- Better Resource Utilization: Threads share the same memory space but can run independently, making efficient use of CPU and memory.
- Responsive User Interfaces: Multithreading allows UI threads to stay responsive while background tasks execute.
- Simplifies Program Design: Some problems are easier to solve when divided into concurrent tasks.
Threads vs Processes
- Process: A program in execution with its own memory space.
- Thread: A lightweight subset of a process that shares the process’s memory but executes independently.
How to Create Threads in Java
Java offers two main ways to create threads:
1. Extending the Thread
Class
You can create a new class that extends Thread
and override its run()
method.
class MyThread extends Thread {
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // starts the new thread and calls run()
}
}
Note: You should call
start()
to begin execution, which internally calls therun()
method. Callingrun()
directly will not start a new thread.
2. Implementing the Runnable
Interface
This is the preferred way because Java supports single inheritance and allows your class to extend another class while implementing Runnable
.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Thread Lifecycle
A thread goes through several states:
- New: Thread is created but not started.
- Runnable: Thread is ready to run and waiting for CPU time.
- Running: Thread is actively executing.
- Blocked/Waiting: Thread is paused, waiting for resources or other threads.
- Terminated: Thread has finished execution.
Synchronization and Thread Safety
Because threads share the same memory space, concurrent access to shared resources can cause problems like race conditions, where the output depends on the unpredictable order of thread execution.
To avoid this, Java provides synchronization mechanisms:
Using synchronized
Keyword
You can mark methods or code blocks to allow only one thread to execute at a time.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Without synchronization, multiple threads incrementing count
might lead to incorrect results.
Example: A Simple Counter with Threads
Let’s build a simple program where multiple threads increment a shared counter safely.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class MultithreadingDemo {
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
try {
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
Explanation:
- We create a
Counter
object shared by two threads. - Each thread increments the counter 1000 times.
- The
increment()
method is synchronized to prevent race conditions. - We use
join()
to wait for both threads to complete before printing the result.
Advanced Threading Concepts (Brief Overview)
- Thread Pools: Manage a pool of reusable threads, reducing overhead of creating new threads.
- Callable and Future: Supports tasks that return results and handle exceptions.
- Executors Framework: Provides high-level API to manage threads and asynchronous tasks.
Best Practices for Multithreading in Java
- Minimize shared mutable state to reduce synchronization needs.
- Use high-level concurrency utilities from
java.util.concurrent
package. - Avoid deadlocks by careful ordering of locks.
- Keep synchronized blocks as short as possible.
- Prefer immutable objects when possible.
Conclusion
Multithreading is a powerful feature that allows your Java applications to perform multiple tasks simultaneously, improving efficiency and responsiveness. This guide covered:
- Basic concepts of threads and processes
- Creating threads using
Thread
class andRunnable
interface - Thread lifecycle
- Synchronization and thread safety
- Practical example with a shared counter
As you gain experience, explore advanced concurrency tools provided by Java to write scalable and efficient multithreaded programs.