Multi-threading and Concurrency in Java
Multi-threading and concurrency are essential aspects of modern programming that allow programs to perform multiple tasks simultaneously, improving performance and responsiveness. Java provides robust support for multi-threading and concurrency through various mechanisms and utilities.
Thread Lifecycle
A thread in Java goes through several states during its lifecycle:
- New: The thread is created but not yet started.
- Runnable: The thread is ready to run and waiting for CPU time.
- Running: The thread is currently executing.
- Blocked: The thread is waiting for a monitor lock to enter a synchronized block/method.
- Waiting: The thread is waiting indefinitely for another thread to perform a particular action.
- Timed Waiting: The thread is waiting for another thread to perform a particular action within a specified waiting time.
- Terminated: The thread has finished its execution.
Creating Threads
There are two main ways to create a thread in Java:
- Extending the
Threadclass:
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();
}
}
- Implementing the
Runnableinterface:
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();
}
}
Synchronization
Synchronization in Java is used to control the access of multiple threads to shared resources. It ensures that only one thread can access the resource at a time, preventing data inconsistency.
Synchronized Methods
A synchronized method locks the object for any thread that enters the method.
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();
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("Final count: " + counter.getCount());
}
}
Synchronized Blocks
A synchronized block locks a specific object, reducing the scope of synchronization and potentially improving performance.
- Example:
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
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("Final count: " + counter.getCount());
}
}
Concurrency Utilities
Executors
The Executor framework provides a way to manage and control thread execution. The ExecutorService interface extends Executor to provide methods for managing the termination of tasks and tracking their progress.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable task1 = () -> {
System.out.println("Executing Task 1");
};
Runnable task2 = () -> {
System.out.println("Executing Task 2");
};
executor.submit(task1);
executor.submit(task2);
executor.shutdown();
}
}
Future and Callable
The Future interface represents the result of an asynchronous computation. The Callable interface is similar to Runnable but can return a result and throw a checked exception.
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;
public class FutureCallableExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 123;
};
Future<Integer> future = executor.submit(task);
try {
System.out.println("Future result: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
Locks
The Lock interface provides more extensive locking operations than synchronized methods and blocks. It is part of the java.util.concurrent.locks package.
- Example:
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;
}
}
public class LockExample {
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("Final count: " + counter.getCount());
}
}
Best Practices for Multi-threading and Concurrency
- Minimize Synchronization: Synchronize only the critical sections of your code to reduce contention and improve performance.
- Use High-level Concurrency Utilities: Prefer using
Executors,Locks, and other high-level concurrency utilities over low-levelThreadandsynchronized. - Avoid Deadlocks: Ensure that your code does not create circular dependencies that lead to deadlocks.
- Use Thread-safe Collections: Use concurrent collections like
ConcurrentHashMapinstead of synchronizing manually. - Use Atomic Variables: For simple operations, use atomic variables like
AtomicIntegerfor better performance and readability. - Handle InterruptedException: Always handle
InterruptedExceptionappropriately, either by propagating it or restoring the interrupt status. - Gracefully Shutdown Executors: Always shut down your
ExecutorServiceusingshutdown()orshutdownNow()to free up resources.
Summary
Multi-threading and concurrency in Java allow you to write efficient and responsive applications. Understanding the thread lifecycle, creating threads using Thread and Runnable, synchronizing shared resources, and leveraging concurrency utilities like Executors, Future, Callable, and Locks are essential for writing robust concurrent programs. By following best practices, you can effectively manage concurrency and avoid common pitfalls such as deadlocks and race conditions.