A Step-by-Step Guide to Java Multithreading
Table of Contents
- Fundamental Concepts of Java Multithreading
- Process vs. Thread
- Thread States
- Synchronization and Race Conditions
- Usage Methods in Java Multithreading
- Creating Threads
- Starting and Stopping Threads
- Thread Priorities
- Common Practices in Java Multithreading
- Producer - Consumer Pattern
- Thread Pools
- Best Practices in Java Multithreading
- Avoiding Deadlocks
- Using Thread - Safe Classes
- Conclusion
- References
Fundamental Concepts of Java Multithreading
Process vs. Thread
- Process: A process is an instance of a program in execution. Each process has its own memory space, system resources, and a single thread of execution (by default). For example, when you open a web browser, a new process is created.
- Thread: A thread is a lightweight sub - process within a process. Multiple threads can exist within a single process, sharing the same memory space and system resources. Threads are more efficient to create and manage compared to processes.
Thread States
Java threads can be in one of the following states:
- New: A thread is in the
Newstate when it is created but not yet started.
Thread newThread = new Thread(() -> System.out.println("New thread running"));
// At this point, newThread is in the New state
- Runnable: When the
start()method is called on a thread, it enters theRunnablestate. It means the thread is ready to run and is waiting for the CPU to allocate time.
newThread.start();
// Now newThread is in the Runnable state
- Blocked: A thread enters the
Blockedstate when it is waiting to acquire a lock. For example, if a thread tries to enter a synchronized block that is already occupied by another thread. - Waiting: A thread can enter the
Waitingstate when it calls methods likewait(),join(), orpark(). It will remain in this state until another thread notifies it. - Timed Waiting: Similar to the
Waitingstate, but the thread will wake up after a specified time. Methods likesleep(long millis)andwait(long millis)can put a thread in theTimed Waitingstate.
try {
Thread.sleep(1000); // Thread is in Timed Waiting state for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
- Terminated: A thread enters the
Terminatedstate when itsrun()method completes or an unhandled exception occurs.
Synchronization and Race Conditions
- Synchronization: In multithreaded programs, multiple threads may access and modify shared resources simultaneously. Synchronization is used to ensure that only one thread can access a shared resource at a time. Java provides the
synchronizedkeyword for this purpose.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
- Race Conditions: A race condition occurs when the behavior of a program depends on the relative timing of events in different threads. For example, if two threads try to increment a shared variable simultaneously without proper synchronization, the final value of the variable may be incorrect.
Usage Methods in Java Multithreading
Creating Threads
There are two main ways to create threads in Java:
- Extending the
Threadclass:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread is running");
}
}
public class ThreadCreationExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- Implementing the
Runnableinterface:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable is running");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
Starting and Stopping Threads
- Starting a thread: As shown above, the
start()method is used to start a thread. It creates a new call stack for the thread and calls therun()method. - Stopping a thread: In Java, there is no direct way to stop a thread. The
stop()method is deprecated because it can leave the program in an inconsistent state. A better way is to use a flag to signal the thread to stop.
class StoppableThread extends Thread {
private volatile boolean stopped = false;
@Override
public void run() {
while (!stopped) {
// Do some work
System.out.println("Thread is running");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stopThread() {
stopped = true;
}
}
public class StopThreadExample {
public static void main(String[] args) {
StoppableThread thread = new StoppableThread();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stopThread();
}
}
Thread Priorities
Threads in Java have priorities that range from 1 (lowest) to 10 (highest). The default priority is 5. You can set the priority of a thread using the setPriority() method.
Thread thread = new Thread(() -> System.out.println("Thread with priority"));
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
Common Practices in Java Multithreading
Producer - Consumer Pattern
The Producer - Consumer pattern is a classic multithreading pattern where one or more producer threads generate data and put it into a shared buffer, and one or more consumer threads take data from the buffer and process it.
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumerExample {
private static final Queue<Integer> buffer = new LinkedList<>();
private static final int MAX_SIZE = 5;
static class Producer implements Runnable {
@Override
public void run() {
try {
while (true) {
synchronized (buffer) {
while (buffer.size() == MAX_SIZE) {
buffer.wait();
}
int item = (int) (Math.random() * 100);
buffer.add(item);
System.out.println("Produced: " + item);
buffer.notifyAll();
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
synchronized (buffer) {
while (buffer.isEmpty()) {
buffer.wait();
}
int item = buffer.poll();
System.out.println("Consumed: " + item);
buffer.notifyAll();
}
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread producerThread = new Thread(new Producer());
Thread consumerThread = new Thread(new Consumer());
producerThread.start();
consumerThread.start();
}
}
Thread Pools
Thread pools are used to manage a group of pre - created threads. Instead of creating a new thread for each task, tasks are submitted to the thread pool, and the pool assigns them to available threads. Java provides the ExecutorService interface and its implementations like ThreadPoolExecutor and Executors for creating thread pools.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
Best Practices in Java Multithreading
Avoiding Deadlocks
A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a lock. To avoid deadlocks:
- Lock Ordering: Always acquire locks in the same order. For example, if two threads need to acquire locks
AandB, both should acquire them in the orderAthenB. - Use
tryLock(): Instead of usinglock()which can block indefinitely, usetryLock()which returns immediately whether the lock is acquired or not.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockAvoidance {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
if (lock1.tryLock()) {
try {
System.out.println("Thread 1 acquired lock1");
if (lock2.tryLock()) {
try {
System.out.println("Thread 1 acquired lock2");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
if (lock1.tryLock()) {
try {
System.out.println("Thread 2 acquired lock1");
if (lock2.tryLock()) {
try {
System.out.println("Thread 2 acquired lock2");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
Using Thread - Safe Classes
Java provides many thread - safe classes like ConcurrentHashMap, CopyOnWriteArrayList, and AtomicInteger. These classes are designed to be used in multithreaded environments without the need for external synchronization.
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeClassExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);
// Multiple threads can safely access and modify this map
}
}
Conclusion
Java multithreading is a powerful feature that can significantly improve the performance and responsiveness of Java applications. By understanding the fundamental concepts, using the right usage methods, applying common practices, and following best practices, developers can write robust and efficient multithreaded code. However, multithreading also introduces complexity such as race conditions and deadlocks, so it should be used with caution.
References
- Oracle Java Documentation: https://docs.oracle.com/javase/8/docs/
- “Effective Java” by Joshua Bloch
- “Java Concurrency in Practice” by Brian Goetz et al.