Introduction
Multithreading is a Java feature that allows concurrent execution of two or more parts of a program to maximize the utilization of CPU. Each part of such a program is called a thread. Threads can be thought of as lightweight processes that enable a program to perform multiple tasks simultaneously.
Table of Contents
- Overview of Multithreading
- Creating Threads
- Extending
Thread
Class - Implementing
Runnable
Interface - Using
Callable
andFuture
- Extending
- Thread Lifecycle
- Thread Priority
- Synchronization
- Synchronized Methods
- Synchronized Blocks
- Static Synchronization
- Inter-thread Communication
wait()
,notify()
, andnotifyAll()
- Thread Pooling
- Using
ExecutorService
- Using
Executors
Factory Methods
- Using
- Advanced Concepts
- Reentrant Locks
- Semaphores
- Barriers
- Best Practices
- Conclusion
1. Overview of Multithreading
Multithreading in Java enables the concurrent execution of two or more threads. By default, every Java program has at least one thread, the main
thread, which is created by the Java Virtual Machine (JVM). Multithreading is primarily used for performing time-consuming operations in the background while keeping the application responsive.
2. Creating Threads
Extending Thread
Class
The simplest way to create a thread is by extending the Thread
class and overriding its run()
method.
Example:
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread
}
}
Implementing Runnable
Interface
Another way to create a thread is by implementing the Runnable
interface. This is generally preferred because it allows the class to extend another class.
Example:
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Start the thread
}
}
Using Callable
and Future
The Callable
interface is similar to Runnable
, but it can return a result and throw a checked exception. The result of the Callable
task can be obtained via a Future
object.
Example:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<String> {
public String call() throws Exception {
return "Thread is running.";
}
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
3. Thread Lifecycle
A thread goes through various states in its lifecycle:
- New: A thread is created but not yet started.
- Runnable: A thread is ready to run and waiting for CPU time.
- Running: A thread is executing.
- Blocked: A thread is blocked waiting for a monitor lock.
- Waiting: A thread is waiting indefinitely for another thread to perform a particular action.
- Timed Waiting: A thread is waiting for another thread to perform a particular action within a stipulated amount of time.
- Terminated: A thread has completed its execution.
4. Thread Priority
Java threads have priority levels that determine the order in which threads are scheduled for execution. Thread priority is an integer ranging from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY). By default, each thread is given priority 5 (NORM_PRIORITY).
Example:
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running with priority " + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setPriority(Thread.MIN_PRIORITY); // Set priority to 1
thread2.setPriority(Thread.MAX_PRIORITY); // Set priority to 10
thread1.start();
thread2.start();
}
}
5. Synchronization
Synchronization is used to control the access of multiple threads to shared resources. It prevents data inconsistency and race conditions.
Synchronized Methods
A synchronized method ensures that only one thread can execute it at a time for a particular object.
Example:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Synchronized Blocks
A synchronized block is a block of code within a method that is synchronized on a specified object, providing more granular control over synchronization.
Example:
class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Static Synchronization
Static synchronization ensures that a class-level lock is acquired, so all instances of the class share the same lock.
Example:
class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
public class StaticSynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
Counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + Counter.getCount());
}
}
6. Inter-thread Communication
Inter-thread communication in Java is achieved using wait()
, notify()
, and notifyAll()
methods. These methods are used to synchronize the activities of multiple threads.
Example:
class SharedResource {
private int data = 0;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait();
}
data = value;
available = true;
notify();
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait();
}
available = false;
notify();
return data;
}
}
public class InterThreadCommunicationExample {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
sharedResource.produce(i);
System.out.println("Produced: " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
int value = sharedResource.consume();
System.out.println("Consumed: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
}
}
7. Thread Pooling
Thread pooling is a technique to reuse a fixed number of threads to execute tasks, which improves the performance of a multithreaded application.
Using ExecutorService
Example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolingExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // Simulate a long-running task
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
Using Executors
Factory Methods
newFixedThreadPool(int nThreads)
: Creates a thread pool with a fixed number of threads.newCachedThreadPool()
: Creates a thread pool that creates new threads as needed but will reuse previously constructed threads when they are available.newSingleThreadExecutor()
: Creates an executor that uses a single worker thread.newScheduledThreadPool(int corePoolSize)
: Creates a thread pool that can schedule commands to run after a given delay or to execute periodically.
8. Advanced Concepts
Reentrant Locks
Reentrant locks are a more flexible way to control synchronization. The ReentrantLock
class provides all the capabilities of synchronized blocks with additional features.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SharedResource {
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;
}
}
public class ReentrantLockExample {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
resource.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + resource.getCount());
}
}
Semaphores
Semaphores are used to control access to a shared resource through the use of counters.
Example:
import java.util.concurrent.Semaphore;
class SharedResource {
private final Semaphore semaphore = new Semaphore(1);
public void accessResource() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " accessed the resource");
Thread.sleep(2000); // Simulate resource access
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
public class SemaphoreExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Runnable task = resource::accessResource;
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
Barriers
Barriers are synchronization aids that allow a set of threads to all wait for each other to reach a common barrier point.
Example:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class BarrierExample {
private static final int PARTIES = 3;
private static final CyclicBarrier barrier = new CyclicBarrier(PARTIES, () -> {
System.out.println("All parties have arrived at the barrier, let's proceed");
});
public static void main(String[] args) {
for (int i = 0; i < PARTIES; i++) {
new Thread(new Task()).start();
}
}
static class Task implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier");
try {
barrier.await();
System.out.println(Thread.currentThread().getName() + " has crossed the barrier");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
9. Best Practices
- Minimize the Scope of Synchronization: Only synchronize the critical section of the code to reduce contention.
- Prefer Synchronized Blocks Over Methods: Provides more granular control over the synchronization.
- Use Thread Pools: Use thread pools to manage a large number of short-lived tasks efficiently.
- Handle InterruptedException Properly: Ensure that your code can gracefully handle interruptions.
- Avoid Nested Locks: Nested locks can lead to deadlocks. Use a single lock or carefully design the lock order.
10. Conclusion
Multithreading in Java is a powerful feature that allows you to perform multiple tasks simultaneously. Understanding the concepts of thread creation, synchronization, inter-thread communication, and advanced synchronization aids is crucial for writing efficient and robust multithreaded applications. By following best practices and leveraging the Java concurrency utilities, you can manage concurrency effectively and build high-performance applications.
Happy coding!
Comments
Post a Comment
Leave Comment