A thread is the smallest unit of execution within a process. In Java, threads can be created by extending the Thread
class or implementing the Runnable
interface. Here is an example of creating a thread using the Runnable
interface:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running.");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
Synchronization is used to control access to shared resources by multiple threads. Java provides the synchronized
keyword to achieve this. Here is an example of a synchronized method:
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();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
Java also provides the Lock
interface and its implementations such as ReentrantLock
for more flexible locking mechanisms compared to the synchronized
keyword. Here is an example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.getCount());
}
}
The Executor framework in Java provides a high - level interface for managing threads. It allows you to create thread pools and submit tasks for execution. Here is an example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task is running.");
}
}
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
executor.submit(new MyTask());
}
executor.shutdown();
}
}
The Callable
interface is similar to Runnable
, but it can return a result. The Future
interface is used to retrieve the result of a Callable
task. Here is an example:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
public class FutureExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
Integer result = future.get();
System.out.println("Result: " + result);
executor.shutdown();
}
}
The producer - consumer pattern is used when one or more threads produce data and one or more threads consume that data. Java provides the BlockingQueue
interface to implement this pattern easily. Here is an example:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
queue.put(i);
System.out.println("Produced: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
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. Here is an example using ReentrantReadWriteLock
:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockExample {
private int data = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(int value) {
lock.writeLock().lock();
try {
data = value;
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
}
Locking can be a performance bottleneck. Try to minimize the amount of code that is locked. For example, in the LockCounter
example above, only the critical section (the increment operation) is locked.
Java provides thread - safe collections such as ConcurrentHashMap
, CopyOnWriteArrayList
, etc. Use these collections instead of non - thread - safe ones in a concurrent environment.
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCollectionExample {
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"));
}
}
Deadlocks occur when two or more threads are waiting for each other to release a lock. To avoid deadlocks, always acquire locks in the same order and use time - out mechanisms when possible.
Java patterns for concurrent programming provide powerful tools and techniques to handle multiple threads efficiently. By understanding the fundamental concepts such as threads, synchronization, and locks, and using the appropriate usage methods like the Executor framework and Future
objects, developers can create high - performance and reliable concurrent applications. Common practices like the producer - consumer pattern and read - write lock pattern can be used to solve specific concurrent problems. Following best practices such as minimizing locking, using thread - safe collections, and avoiding deadlocks is essential for writing robust concurrent code.