Multithreading and Concurrency in Java
Eugene Koshy
Software Engineering Manager | Oracle Banking Solutions Expert | Data Analytics Specialist | PL/SQL Expert
What is Multithreading?
Multithreading is the ability of a CPU (or a single core in a multi-core processor) to execute multiple threads concurrently. In Java, a thread is the smallest unit of execution within a process. Multithreading allows a program to perform multiple tasks simultaneously, enhancing performance and responsiveness.
Why Use Multithreading?
Multithreading is essential for building efficient, high-performance applications. Some key advantages include:
? Improved Performance - Leverages multi-core processors for parallel execution, speeding up computation-heavy tasks.
? Responsiveness - Keeps applications interactive by running background tasks separately (e.g., GUI applications that perform background computations).
? Better Resource Utilization - Enables sharing of system resources more effectively.
? Simplified Modeling - Naturally represents real-world concurrent processes like handling multiple network requests.
Concurrency vs. Parallelism
Threads vs. Processes
Creating Threads in Java
There are two main ways to create threads in Java:
1?? Extending the Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
2?? Implementing the Runnable Interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
? Best Practice: Using Runnable is preferable as it allows flexibility (Java supports only single inheritance, but interfaces can be implemented multiple times).
Thread Lifecycle
A thread can be in one of the following states:
?? New - Created but not started.
?? Runnable - Ready to run, waiting for CPU time.
?? Running - Actively executing its task.
?? Blocked/Waiting - Paused, waiting for a resource.
?? Terminated - Completed execution.
Thread Priorities
Threads can have priorities ranging from 1 (lowest) to 10 (highest):
thread.setPriority(Thread.MAX_PRIORITY); // 10
Synchronization in Multithreading
?? Race Conditions
A race condition occurs when multiple threads access shared data concurrently, leading to unpredictable results.
? Synchronized Methods and Blocks
Use the synchronized keyword to ensure only one thread accesses a critical section at a time:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
?? Intrinsic Locks (Monitor Locks)
Every Java object has an intrinsic lock (monitor). When a thread enters a synchronized block, it acquires the lock and releases it upon exit.
Inter-Thread Communication
Java provides wait(), notify(), and notifyAll() for inter-thread communication:
wait(): Releases the lock and waits for notification.
notify(): Wakes up a single waiting thread.
notifyAll(): Wakes up all waiting threads.
class Buffer {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) wait();
data = value;
available = true;
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (!available) wait();
available = false;
notifyAll();
return data;
}
}
Java Concurrency Utilities
1?? ExecutorService and Thread Pools
The ExecutorService framework simplifies thread management:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("Task executed"));
executor.shutdown();
2?? Callable and Future
Callable is a task that returns a result, and Future represents the result of an asynchronous computation:
Callable<Integer> task = () -> 42;
Future<Integer> future = executor.submit(task);
System.out.println(future.get()); // 42
CountDownLatch: Waits for a set of tasks to complete.
CyclicBarrier: Synchronizes threads at a common point.
Semaphore: Controls access to a shared resource.
3?? Locks and Synchronization Tools
? ReentrantLock : provides more flexibility than intrinsic locks:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
? ReadWriteLock : Allows multiple readers or a single writer:
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
lock.writeLock().lock();
? StampedLock (Optimistic Locking): An advanced lock that supports optimistic locking:
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
? Thread Safety (Immutable Objects): Immutable objects are inherently thread-safe:
public final class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
? Thread-Local Variables: Each thread has its own copy of a thread-local variable
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
? Atomic Variables: provide thread-safe operations without locks
public final class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Concurrent Collections
1?? ConcurrentHashMap
A thread-safe version of HashMap:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 42);
2?? CopyOnWriteArrayList
A thread-safe version of ArrayList:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("value");
3?? BlockingQueue
A thread-safe queue that supports blocking operations
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("value");
String value = queue.take();
Fork/Join Framework
?? ForkJoinPool
A thread pool for parallel execution:
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new MyTask());
class MyTask extends RecursiveTask<Integer> {
protected Integer compute() {
return 42;
}
}
?? RecursiveTask
Tasks that can be split into smaller subtasks
class MyTask extends RecursiveTask<Integer> {
protected Integer compute() {
return 42;
}
}
Common Concurrency Issues
? Deadlocks - Occurs when two threads hold locks that the other needs.
? Livelocks - Threads actively change state but fail to make progress.
? Starvation - A thread is unable to gain access to a resource due to high-priority threads monopolizing it.
?? Real-World Applications of Multithreading
? Web Servers - Handle multiple client requests concurrently.
? Big Data Processing - Process large datasets in parallel.
? Real-Time Systems - Ensure timely execution of critical tasks.
? Gaming - Manages physics, rendering, and AI in separate threads.
?? This article provides a comprehensive overview of Multithreading and Concurrency in Java with theoretical concepts, practical examples, and best practices.
?? What are your experiences with multithreading in Java? Let’s discuss in the comments!
If you found this article helpful, feel free to share it with your network. For more in-depth tutorials on Java and other programming topics, follow me on LinkedIn.