Multithreading and Concurrency in Java
Introduction to Multithreading
Multithreading is a feature in Java that allows concurrent execution of two or more parts of a program, known as threads. Each thread runs in parallel and can handle tasks simultaneously, making programs more efficient, especially in multi-core systems.
Key Concepts:
- Thread: The smallest unit of processing. In Java, every thread has a lifecycle: New, Runnable, Running, Blocked, and Terminated.
- Concurrency: Executing multiple threads simultaneously. Helps in performing multiple tasks at the same time.
- Parallelism: Actual simultaneous execution of tasks in a multi-core processor.
Benefits of Multithreading
- Improved performance: By utilizing multiple threads, a program can perform several tasks concurrently, potentially improving its performance.
- Resource sharing: Threads within the same process share resources, such as memory, which can lead to more efficient resource usage.
- Simplified modeling: Multithreading can simplify the modeling of certain tasks, like handling user interfaces or performing background tasks.
Drawbacks of Multithreading
- Complexity: Managing multiple threads can make a program more complex and harder to debug.
- Synchronization issues: Threads that share resources can lead to synchronization problems if not managed correctly.
- Increased resource consumption: Creating and managing multiple threads can consume more resources than a single-threaded approach.
Creating and Running Threads
In Java, you can create threads in two ways:
- Extending the
Thread
class: - Implementing the
Runnable
interface:
Extending the Thread
Class
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(); // Starts the thread, which calls the run() method
}
}
Implementing the Runnable
Interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // Starts the thread, which calls the run() method
}
}
Thread Life Cycle
A thread in Java goes through various states in its life cycle:
- New: When a thread is created but not yet started.
- Runnable: When a thread is ready to run but waiting for CPU time.
- Blocked: When a thread is blocked waiting for a monitor lock.
- Waiting: When a thread is waiting indefinitely for another thread to perform a particular action.
- Timed Waiting: When a thread is waiting for a specified amount of time.
- Terminated: When a thread has finished its execution.
Synchronization
Synchronization in Java is a mechanism to control the access of multiple threads to shared resources. This is crucial to avoid inconsistent data and ensure thread safety.
Synchronized Method
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Synchronized Block
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
Inter-Thread Communication
Java provides methods like wait()
, notify()
, and notifyAll()
for communication between threads.
Example
class SharedResource {
private int value = 0;
private boolean available = false;
public synchronized void produce(int newValue) {
while (available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
value = newValue;
available = true;
notify();
}
public synchronized int consume() {
while (!available) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
available = false;
notify();
return value;
}
}
Concurrency Utilities
Java’s java.util.concurrent
package provides higher-level concurrency utilities.
Executor Framework
The Executor framework provides a way to manage a pool of threads, making it easier to manage multiple threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
public void run() {
System.out.println("Task is running.");
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executor.execute(new MyTask());
}
executor.shutdown();
}
}
Locks
Java provides the Lock
interface and its implementation classes (ReentrantLock
, ReadWriteLock
) to have more control over synchronization.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
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;
}
}
Atomic Variables
The java.util.concurrent.atomic
package provides classes like AtomicInteger
, AtomicLong
, etc., for lock-free thread-safe operations on single variables.
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
By understanding and utilizing these concepts and tools, you can effectively manage concurrency and multithreading in Java applications.