Multi-threading and concurrency in Java allow you to execute multiple threads simultaneously, enabling efficient utilization of CPU resources and improving application performance. Java provides robust support for multi-threading through its Thread
class, Runnable
interface, and the java.util.concurrent
package.
1. Basics of Multi-threading
- Thread: A lightweight sub-process that executes independently.
- Main Thread: The default thread created when a Java program starts.
- Concurrency: The ability to run multiple threads in parallel or in an interleaved manner.
2. Creating Threads
There are two ways to create threads in Java:
- Extending the
Thread
class. - Implementing the
Runnable
interface.
a. Extending the Thread
Class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread
}
}
b. Implementing the Runnable
Interface
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // Start the thread
}
}
3. Thread Lifecycle
A thread can be in one of the following states:
- New: The thread is created but not started.
- Runnable: The thread is executing or ready to execute.
- Blocked/Waiting: The thread is waiting for a resource or another thread.
- Timed Waiting: The thread is waiting for a specified time.
- Terminated: The thread has completed execution.
4. Thread Synchronization
When multiple threads access shared resources, synchronization is required to avoid race conditions and ensure thread safety.
a. Synchronized Methods
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
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("Count: " + counter.getCount()); // Output: 2000
}
}
b. Synchronized Blocks
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
5. Thread Communication
Threads can communicate with each other using wait()
, notify()
, and notifyAll()
methods.
Example: Producer-Consumer Problem
class SharedResource {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait(); // Wait for the consumer to consume
}
data = value;
available = true;
notify(); // Notify the consumer
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait(); // Wait for the producer to produce
}
available = false;
notify(); // Notify the producer
return data;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Runnable producer = () -> {
try {
for (int i = 0; i < 5; i++) {
resource.produce(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable consumer = () -> {
try {
for (int i = 0; i < 5; i++) {
int value = resource.consume();
System.out.println("Consumed: " + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
6. Thread Pools
Thread pools manage a pool of worker threads to execute tasks efficiently. Java provides the ExecutorService
interface for thread pool management.
Example: Using ExecutorService
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable task1 = () -> System.out.println("Task 1 executed by " + Thread.currentThread().getName());
Runnable task2 = () -> System.out.println("Task 2 executed by " + Thread.currentThread().getName());
Runnable task3 = () -> System.out.println("Task 3 executed by " + Thread.currentThread().getName());
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown(); // Shutdown the executor
}
}
7. Concurrent Collections
Java provides thread-safe collections in the java.util.concurrent
package, such as ConcurrentHashMap
, CopyOnWriteArrayList
, and BlockingQueue
.
Example: Using ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
System.out.println("Map: " + map);
}
}
8. Atomic Variables
Atomic variables (e.g., AtomicInteger
, AtomicLong
) provide thread-safe operations without synchronization.
Example: Using AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
public static void main(String[] args) throws InterruptedException {
AtomicInteger counter = new AtomicInteger(0);
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Counter: " + counter.get()); // Output: 2000
}
}
9. Deadlock
Deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources.
Example: Deadlock
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1");
}
}
});
thread1.start();
thread2.start();
}
}
10. Best Practices
- Prefer
Runnable
over extending theThread
class. - Use thread pools for efficient thread management.
- Use concurrent collections for thread-safe data structures.
- Avoid deadlock by acquiring locks in a consistent order.
- Use atomic variables for simple thread-safe operations.
By mastering multi-threading and concurrency in Java, you can build high-performance, scalable, and responsive applications!