MULTITHREADING
Multithreading is a powerful concept in Java that allows us to run multiple threads concurrently within a single
process. It’s crucial for developing responsive and efficient applications, especially in today’s multi-core
processor environments. In this comprehensive guide, we’ll dive deep into multithreading, covering theory and
practical implementation, making us proficient in this essential aspect of Java programming.
What is Multithreading?
Multithreading is a programming concept that allows a single process to execute multiple threads concurrently.
Threads are lightweight sub-processes within a process that share the same memory space, but they can run
independently. Each thread represents a separate flow of control, making it possible to perform multiple tasks
simultaneously within a single program.
Key Points:
Threads are smaller units of a process, sharing the same memory space. Threads can be thought of as
independent, parallel execution paths.
Multithreading enables efficient utilization of multi-core processors.
Threads are smaller units of a process, sharing the same memory space.
Threads can be thought of as independent, parallel execution paths.
Multithreading enables efficient utilization of multi-core processors.
Why Use Multithreading?
Multithreading offers several advantages, making it a valuable tool in software development:
Improved Responsiveness: Multithreading allows applications to remain responsive to user input, even when
performing resource-intensive tasks. For example, a text editor can continue responding to user actions while
performing a spell-check in the background.
Enhanced Performance: Multithreaded programs can take advantage of multi-core processors, leading to better
performance. Tasks can be divided among multiple threads, speeding up computation.
Resource Sharing: Threads can share data and resources within the same process, which can lead to more
efficient memory usage. This can be crucial in memory-intensive applications.
Concurrency: Multithreading enables concurrent execution of tasks, making it easier to manage multiple tasks
simultaneously. For instance, a web server can handle multiple client requests concurrently using threads.
Terminology and Concepts
To understand multithreading, it’s essential to grasp the following key concepts:
Thread: A thread is the smallest unit of execution within a process. Multiple threads can exist within a
single process and share the same memory space.
Process: A process is an independent program that runs in its memory space. It can consist of one or
multiple threads.
Concurrency: Concurrency refers to the execution of multiple threads in overlapping time intervals. It
allows tasks to appear as if they are executing simultaneously.
Parallelism: Parallelism involves the actual simultaneous execution of multiple threads or processes,
typically on multi-core processors. It achieves true simultaneous execution.
Race Condition: A race condition occurs when two or more threads access shared data concurrently, and
the final outcome depends on the timing and order of execution. It can lead to unpredictable behavior and
bugs.
Synchronization: Synchronization is a mechanism used to coordinate and control access to shared
resources. It prevents race conditions by allowing only one thread to access a resource at a time.
Deadlock: Deadlock is a situation in which two or more threads are unable to proceed because each is
waiting for the other to release a resource. It can result in a system freeze.
Creating Threads in Java
Extending the Thread Class
Implementing the Runnable Interface
In Java, we can create threads in two main ways: by extending the Thread class or by implementing
the Runnable interface. Both methods allow us to define the code that runs in the new thread.
Extending the Thread Class:
1
To create a thread by extending the Thread class, we need to create a new class that inherits from Thread and
overrides the run() method. The run() method contains the code that will execute when the thread starts. Here’s an
example:
class MyThread extends Thread {
public void run() {
// Code to be executed in the new thread
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + Thread.currentThread().getId() + ": Count " + i);
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // Starts the first thread
thread2.start(); // Starts the second thread
}
}
In this example, we create a MyThread class by extending Thread. The run() method contains the code to be
executed in the new thread. We create two instances of MyThread and start them using the start() method.
Implementing the Runnable Interface:
An alternative and often more flexible way to create threads is by implementing the Runnable interface. This
approach allows us to separate the thread’s behavior from its structure, making it easier to reuse and extend.
Here’s an example:
class MyRunnable implements Runnable {
public void run() {
// Code to be executed in the new thread
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + Thread.currentThread().getId() + ": Count " + i);
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
thread1.start(); // Starts the first thread
thread2.start(); // Starts the second thread
}
}
2
In this example, we create a MyRunnable class that implements the Runnable interface. The run() method
contains the code to be executed in the new thread. We create two Thread instances, passing the MyRunnable
instance as a constructor argument. Then, we start both threads.
The Thread Lifecycle:
Threads in Java go through various states in their lifecycle:
New: When a thread is created but not yet started.
Runnable: The thread is ready to run and is waiting for its turn to execute.
Running: The thread is actively executing its code.
Blocked/Waiting: The thread is temporarily inactive, often due to waiting for a resource or event.
Terminated: The thread has completed execution and has been terminated.
Understanding the thread lifecycle is essential for proper thread management and synchronization in
multithreaded applications.
Working with Multiple Threads
When working with multiple threads, we need to be aware of various challenges and concepts, including thread
interference, deadlocks, thread priority, and thread groups.
Thread Interference:
Thread interference occurs when multiple threads access shared data concurrently, leading to unexpected and
incorrect results. To avoid thread interference, we can use synchronization mechanisms like synchronized blocks
or methods to ensure that only one thread accesses the shared data at a time. Here's a simple example illustrating
thread interference:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class ThreadInterferenceExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
3
System.out.println("Final Count: " + counter.getCount());
}
}
In this example, two threads (thread1 and thread2) increment a shared counter concurrently. To prevent
interference, we use synchronized methods for incrementing and getting the count.
Deadlocks and Solutions:
A deadlock occurs when two or more threads are unable to proceed because they are each waiting for the other to
release a resource. Deadlocks can be challenging to diagnose and fix. Strategies to prevent deadlocks include
using proper locking orders, timeouts, and deadlock detection algorithms. Here’s a high-level example of a
potential deadlock scenario:
class Resource {
public synchronized void method1(Resource other) {
// Do something
other.method2(this);
// Do something
}
public synchronized void method2(Resource other) {
// Do something
other.method1(this);
// Do something
}
}
public class DeadlockExample {
public static void main(String[] args) {
Resource resource1 = new Resource();
Resource resource2 = new Resource();
Thread thread1 = new Thread(() -> resource1.method1(resource2));
Thread thread2 = new Thread(() -> resource2.method1(resource1));
thread1.start();
thread2.start();
}
}
In this example, thread1 calls method1 on resource1, while thread2 calls method1 on resource2. Both methods
subsequently attempt to acquire locks on the other resource, leading to a potential deadlock situation.
Thread Priority and Group:
Java allows us to set thread priorities to influence the order in which threads are scheduled for execution by the
JVM’s thread scheduler. Threads with higher priorities are given preference, although it’s essential to use thread
priorities judiciously, as they may not behave consistently across different JVM implementations. Additionally,
we can group threads for better management and control.
4
Thread thread1 = new Thread(() -> {
// Thread 1 logic
});
Thread thread2 = new Thread(() -> {
// Thread 2 logic
});
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MIN_PRIORITY);
ThreadGroup group = new ThreadGroup("MyThreadGroup");
Thread thread3 = new Thread(group, () -> {
// Thread 3 logic
});
In this example, we set thread priorities for thread1 and thread2 and create a thread group named
"MyThreadGroup" for thread3. Thread priorities range from Thread.MIN_PRIORITY (1)
to Thread.MAX_PRIORITY (10).
Understanding and effectively managing thread interference, deadlocks, thread priorities, and thread groups are
crucial when working with multiple threads in Java.
Java’s Concurrency Utilities
Java provides a set of powerful concurrency utilities that simplify the development of multithreaded applications.
Three fundamental components of Java’s concurrency utilities are the Executor Framework, Thread Pools, and
Callable and Future.
The Executor Framework:
The Executor Framework is an abstraction layer for managing the execution of tasks asynchronously in a
multithreaded environment. It decouples task submission from task execution, allowing us to focus on what needs
to be done rather than how it should be executed.
Here’s an example of how to use the Executor Framework to execute tasks:
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
Executor executor = Executors.newSingleThreadExecutor();
Runnable task = () -> {
System.out.println("Task is executing...");
};
executor.execute(task);
}
}
In this example, we create an Executor using Executors.newSingleThreadExecutor(), which creates a single-
threaded executor. We then submit a Runnable task to be executed asynchronously.
Thread Pools:
5
Thread pools are a mechanism for managing and reusing a fixed number of threads to execute tasks. They
provide better performance compared to creating a new thread for each task, as thread creation and destruction
overhead are reduced.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable task1 = () -> {
System.out.println("Task 1 is executing...");
};
Runnable task2 = () -> {
System.out.println("Task 2 is executing...");
};
executorService.submit(task1);
executorService.submit(task2);
executorService.shutdown();
}
}
In this example, we create a fixed-size thread pool with two threads and submit two tasks for execution.
The shutdown method is called to gracefully shut down the thread pool when it's no longer needed.
Callable and Future:
The Callable interface is similar to Runnable, but it can return a result or throw an exception. The Future interface
represents the result of an asynchronous computation and provides methods to retrieve the result or handle
exceptions.
import java.util.concurrent.*;
public class CallableAndFutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
Thread.sleep(2000);
return 42;
};
Future<Integer> future = executorService.submit(task);
System.out.println("Waiting for the result...");
Integer result = future.get();
System.out.println("Result: " + result);
6
executorService.shutdown();
}
}
In this example, we create a Callable task that sleeps for 2 seconds and returns the value 42. We submit the task
to an executor, and then we use the get method Future to wait for the result of the computation.
These Java concurrency utilities provide a powerful and efficient way to manage multithreaded tasks, thread
pools, and asynchronous computations in our applications.
Advanced Multithreading
In advanced multithreading, we dive deeper into the intricacies of managing threads, handling synchronization,
and leveraging advanced concurrency features in Java.
Daemon Threads:
Daemon threads are background threads that run in the background of a Java application. They are typically used
for non-critical tasks and do not prevent the application from exiting when the main program finishes execution.
Thread daemonThread = new Thread(() -> {
while (true) {
// Perform background tasks
}
});
daemonThread.setDaemon(true); // Set as a daemon thread
daemonThread.start();
Thread Local Variables:
Thread-local variables are variables that are local to each thread. They allow us to store data that is specific to a
particular thread, ensuring that each thread has its own independent copy of the variable.
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
threadLocal.set(42); // Set thread-local value
int value = threadLocal.get(); // Get thread-local value
Thread States (WAITING, TIMED_WAITING, BLOCKED):
Threads can be in different states, including WAITING, TIMED_WAITING, and BLOCKED. These states
represent various scenarios where threads are waiting for resources or conditions to change.
Inter-thread Communication:
Inter-thread communication allows threads to coordinate and exchange information. This includes the use
of wait, notify, and notifyAll methods to synchronize threads.
Concurrent Collections:
Concurrent collections are thread-safe data structures that allow multiple threads to access and modify them
concurrently without causing data corruption or synchronization issues. Some examples
include ConcurrentHashMap, ConcurrentLinkedQueue, and CopyOnWriteArrayList.
Thread Safety and Best Practices:
Ensuring thread safety is crucial in multithreaded applications. This section covers best practices such as using
immutable objects, atomic classes, and avoiding string interning to write robust and thread-safe code.
Parallelism in Java:
Parallelism involves executing tasks concurrently to improve performance. Java provides features like parallel
streams in Java 8 and CompletableFuture for asynchronous programming.
Real-world Multithreading:
7
This section delves into real-world applications of multithreading, including implementing a web server and
using multithreading in game development.
These advanced topics in multithreading equip developers with the knowledge and skills needed to build
efficient, concurrent, and scalable applications in Java. Each topic is accompanied by examples to illustrate the
concepts and techniques.
File Name: MultiThreadingExample.java
class MyThread extends Thread {
private String threadName;
MyThread(String name) {
threadName = name;
}
// Override the run method to define the task for the thread
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
// Sleep for a while to simulate some work
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted.");
}
}
System.out.println(threadName + " finished.");
}
}
public class MultiThreadingExample {
public static void main(String[] args) {
// Create instances of MyThread
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
MyThread thread3 = new MyThread("Thread 3");
// Start the threads
thread1.start();
thread2.start();
thread3.start();
// Wait for all threads to finish
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("All threads have finished.");
}
}
8
Output:
Thread 3 - Count: 1
Thread 1 - Count: 1
Thread 2 - Count: 1
Thread 3 - Count: 2
Thread 2 - Count: 2
Thread 1 - Count: 2
Thread 3 - Count: 3
Thread 2 - Count: 3
Thread 1 - Count: 3
Thread 3 - Count: 4
Thread 1 - Count: 4
Thread 2 - Count: 4
Thread 3 - Count: 5
Thread 2 - Count: 5
Thread 1 - Count: 5
Thread 1 finished.
Thread 3 finished.
Thread 2 finished.
All threads have finished.
Explanation
By extending the Thread class and overriding its run method to print a message
five times with a one-second delay in between, this Java programme illustrates
multithreading.
Three instances of MyThread are generated and launched using the start function
in the main class, MultiThreadingExample, enabling them to operate concurrently.
In order to guarantee that the main thread waits for every thread to finish before
issuing a final message stating that every thread has finished, the join() method
is employed.