Java Multithreading is a process where multiple threads are executed simultaneously in a Java program. A thread is a lightweight subprocess, and multithreading allows concurrent execution of two or more parts of a program for maximum utilization of CPU resources.
Why Use Multithreading?
- Improved Performance: It allows better CPU utilization, especially when tasks can be parallelized.
- Concurrent Task Execution: Multiple threads can execute different tasks simultaneously.
- Simplified Modeling: For complex systems with multiple tasks (e.g., GUI applications), multithreading simplifies the model by handling different tasks concurrently.
- Responsive Applications: Multithreading allows applications to remain responsive, such as GUIs where background operations like file downloads don’t freeze the interface.
Key Concepts of Multithreading
-
Thread: A thread is the smallest unit of a process. Java has built-in support for threads via the
Thread
class and theRunnable
interface. -
Concurrency: The concept of executing multiple tasks concurrently (simultaneously or seemingly simultaneously).
-
Parallelism: It refers to actual simultaneous execution of multiple threads on different cores.
-
Context Switching: The process of switching between threads, controlled by the CPU scheduler.
Java Multithreading Basics
1. Creating a Thread by Extending the Thread
Class
You can create a thread by extending the Thread
class and overriding its run()
method:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MyThread extends Thread { public void run() { System.out.println("Thread is running..."); } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // Start the thread } } |
2. Creating a Thread by Implementing Runnable
Interface
Implementing Runnable
is a more flexible approach because it allows the class to extend other classes.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MyRunnable implements Runnable { public void run() { System.out.println("Thread is running..."); } } public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // Start the thread } } |
3. Thread States
A thread can be in one of the following states:
- New: When a thread object is created but not started yet.
- Runnable: When a thread is ready to run and is waiting for CPU time.
- Blocked/Waiting: When a thread is waiting for a resource or another thread to complete.
- Timed Waiting: When a thread waits for a specified period.
- Terminated: When a thread completes its execution.
4. Thread Methods
start()
: Starts the execution of the thread by invoking therun()
method.run()
: Contains the code that defines the behavior of the thread.sleep(milliseconds)
: Makes the current thread sleep for a specified amount of time.join()
: Waits for a thread to die.yield()
: Pauses the current thread to allow other threads to execute.interrupt()
: Interrupts the thread and can stop it if it’s blocked.
Synchronization in Multithreading
In a multithreaded environment, it’s crucial to ensure thread safety when multiple threads access shared resources, such as objects or variables. Java provides synchronization mechanisms to avoid race conditions and ensure that only one thread accesses critical sections of code at a time.
1. Synchronized Methods
When a method is marked as synchronized
, only one thread can execute it at a time for a particular object.
1 2 3 4 5 6 7 8 9 10 11 |
public class Counter { private int count = 0; public synchronized void increment() { count++; } public int getCount() { return count; } } |
2. Synchronized Blocks
A synchronized block is more granular and locks only a specific part of the code instead of the whole method.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Counter { private int count = 0; public void increment() { synchronized (this) { count++; } } public int getCount() { return count; } } |
3. Volatile Keyword
The volatile
keyword ensures that the value of a variable is always read from the main memory rather than the CPU cache. This is important for shared variables between threads.
1 2 3 4 5 6 7 8 9 10 11 |
public class SharedData { private volatile boolean flag = false; public void setFlag(boolean value) { this.flag = value; } public boolean isFlag() { return flag; } } |
Thread Pooling
Creating and destroying threads repeatedly can be expensive. Thread pooling allows you to reuse a fixed number of threads, reducing the overhead of thread management.
Example using ExecutorService
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Create a pool of 3 threads for (int i = 0; i < 5; i++) { executor.submit(() -> { System.out.println("Thread " + Thread.currentThread().getName() + " is running"); }); } executor.shutdown(); // Shutdown the executor after task completion } } |
Locks and Semaphores
Locks provide more extensive locking mechanisms than synchronized methods. The java.util.concurrent.locks
package provides Lock
and ReentrantLock
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SafeCounter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { return count; } } |
Semaphore controls access to resources, allowing a fixed number of threads to access a resource simultaneously.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.util.concurrent.Semaphore; public class SemaphoreExample { private static Semaphore semaphore = new Semaphore(3); public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { try { semaphore.acquire(); System.out.println("Thread " + Thread.currentThread().getName() + " is running"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }).start(); } } } |
Advanced: ForkJoinPool and Parallel Streams
Java 8 introduced ForkJoinPool for parallel tasks and parallel streams to automatically divide tasks into subtasks.
1 2 |
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.parallelStream().forEach(System.out::println); |
Conclusion
Multithreading in Java is a powerful concept that allows you to perform tasks concurrently and improve performance. However, it requires careful management of shared resources using synchronization and proper thread handling mechanisms like thread pools, locks, and semaphores to avoid issues such as race conditions, deadlocks, and resource contention.