In Java, a Thread
is an instance of the java.lang.Thread
class, representing a single thread of execution. The Runnable
interface is used to define a task that can be executed by a thread. You can create a thread by either extending the Thread
class or implementing the Runnable
interface.
// Implementing Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running thread from Runnable");
}
}
// Extending Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running thread from extended Thread");
}
}
public class ThreadConceptExample {
public static void main(String[] args) {
// Using Runnable
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
thread1.start();
// Using extended Thread
MyThread myThread = new MyThread();
myThread.start();
}
}
Synchronization is a mechanism used to control access to shared resources in a multi - threaded environment. In Java, the synchronized
keyword can be used in two ways: as a method modifier or as a block.
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public void incrementUsingBlock() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
A thread pool is a collection of pre - created threads that are ready to execute tasks. It helps in reducing the overhead of creating and destroying threads repeatedly. Java provides the ExecutorService
interface and its implementations, such as ThreadPoolExecutor
, to manage thread pools.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
@Override
public void run() {
System.out.println("Executing task in thread pool");
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(new Task());
}
executor.shutdown();
}
}
The producer - consumer pattern is used when one or more threads produce data and one or more threads consume that data. A shared buffer is used to store the data.
import java.util.LinkedList;
import java.util.Queue;
class Buffer {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity = 5;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait();
}
queue.add(item);
System.out.println("Produced: " + item);
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notifyAll();
return item;
}
}
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
buffer.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
buffer.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producerThread = new Thread(new Producer(buffer));
Thread consumerThread = new Thread(new Consumer(buffer));
producerThread.start();
consumerThread.start();
}
}
The read - write lock pattern allows multiple threads to read a shared resource simultaneously, but only one thread can write to it at a time.
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class SharedResource {
private int data = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(int newData) {
lock.writeLock().lock();
try {
data = newData;
System.out.println("Written: " + data);
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
System.out.println("Read: " + data);
return data;
} finally {
lock.readLock().unlock();
}
}
}
public class ReadWriteLockExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread writer = new Thread(() -> resource.write(10));
Thread reader1 = new Thread(() -> resource.read());
Thread reader2 = new Thread(() -> resource.read());
writer.start();
reader1.start();
reader2.start();
}
}
Deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources. To avoid deadlocks, follow these practices:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class DeadlockAvoidance {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
boolean locked1 = lock1.tryLock();
if (locked1) {
try {
boolean locked2 = lock2.tryLock();
if (locked2) {
try {
// Do some work
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
}
The volatile
keyword is used to ensure that a variable is always read from and written to the main memory, rather than from a thread’s local cache. It is useful for variables that are accessed by multiple threads.
class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean getFlag() {
return flag;
}
}
Java provides high - level concurrency utilities in the java.util.concurrent
package, such as ConcurrentHashMap
, CountDownLatch
, and CyclicBarrier
. These utilities are more robust and easier to use than low - level synchronization mechanisms.
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);
System.out.println(map.get("key1"));
}
}
Creating too many threads can lead to resource exhaustion and poor performance. Use thread pools and limit the number of threads based on the available resources.
In multi - threaded programming, proper error handling is crucial. Catch exceptions in the appropriate threads and log them for debugging purposes.
class ErrorHandlingExample implements Runnable {
@Override
public void run() {
try {
// Code that may throw an exception
int result = 1 / 0;
} catch (Exception e) {
System.err.println("Exception in thread: " + Thread.currentThread().getName() + ": " + e.getMessage());
}
}
}
Java multi - threading design patterns are essential tools for developers working with multi - threaded applications. By understanding the fundamental concepts, usage methods, common practices, and best practices, developers can write more efficient, robust, and maintainable multi - threaded code. These patterns help in solving common problems such as race conditions, deadlocks, and resource contention, ensuring that the application performs well under concurrent access.