Multithreading Basics (Java examples by chatgpt)
Multithreading
Multithreading is a programming technique that allows multiple threads to run concurrently and parallely within a single process. Parallelism is possible if you have multiple cores available for the threads to run on.
Threads are lightweight execution units within a process. They share the same memory space, resources, and state as the parent process. Each thread has its own program counter, stack, and set of registers, allowing it to execute instructions independently.
Threads within a process share the same resources, such as memory, files, and network connections. While this can provide efficiency, it also requires careful synchronization to ensure that multiple threads don’t interfere with each other or access shared resources simultaneously, leading to data inconsistencies or race conditions.
Multithreading at the operating system level gives an illusion of simultaneous execution of multiple applications. The scheduler switches between different threads allowing them to progress concurrently.
Multithreading can provide both parallelism and concurrency.
- A web browser is a concurrent application. Multiple tabs can be open at the same time, and the user can switch between them. However, only one tab can be actively running at a time.
- A video game is a parallel application. Multiple processes can be running at the same time, such as the game engine, the physics engine, and the graphics engine. This allows the game to run smoothly even when there is a lot of action happening on screen.

Java Specifics
Creating and Terminating threads
Thread is a class in Java that represents a thread of execution.
To create a new thread using the Thread class, you can extend the Thread class and override the run() method, which contains the code that will be executed by the thread.
Once you’ve created a Thread object, you can start the execution of the thread by calling the start() method on the Thread object.
The Thread class provides additional methods and functionalities for managing threads, such as controlling thread execution, pausing/resuming threads, and checking thread status.
Runnable is an interface in Java that represents a task or unit of work that can be executed by a thread.
To use the Runnable interface, you need to implement the run() method defined by the interface. This method contains the code that will be executed when the thread runs.
Once you have implemented the run() method, you can create a Thread object, passing an instance of the Runnable implementation to the Thread constructor.
The Thread object created with a Runnable can execute the task defined in the run() method when the thread is started with the start() method.
In summary, Thread is a class that represents a thread of execution, while Runnable is an interface that defines the task to be executed by a thread. You can create a thread by either extending the Thread class or implementing the Runnable interface and passing it to a Thread object. Using the Runnable interface allows for better separation of concerns, as it decouples the task from the thread implementation. This makes it easier to reuse the task implementation and promotes a more flexible and modular design.
Runnable r = new RunnableImplementation();
Thread t = new Thread(r);
t.start();
Daemon thread is a type of thread that runs in the background and provides support to other non-daemon threads. Unlike regular (non-daemon) threads, the JVM does not wait for daemon threads to complete before terminating the program.
Thread daemonThread = new Thread(new Runnable() {
public void run() {
// Background task implementation
}
});
daemonThread.setDaemon(true);
daemonThread.start();
We need to explicitly terminate non-daemon threads.
We can use a shared flag that a thread can periodically check to terminate its execution
public class MyThread extends Thread {
private volatile boolean isStopped = false;
public void run() {
while (!isStopped) {
// Perform the main task of the thread
System.out.println("Thread is running...");
// thread performing some task
}
System.out.println("Thread stopped");
}
public void stopThread() {
isStopped = true;
}
}
MyThread thread = new MyThread();
thread.start();
//Thread runs for a while
// Request the thread to stop
thread.stopThread();
Marking isStopped as volatile ensures that changes made to the variable by one thread are immediately visible to other threads.
When a thread is in blocked state we can’t expect it to check flag value.
In such cases, you can call the interrupt() method on the thread object. It sets the interrupted status of the thread to true. The thread needs to be responsive to interruption requests by checking the interrupted status using Thread.interrupted() or Thread.isInterrupted().
public class MyThread extends Thread {
public void run() {
while (!Thread.interrupted()) {
// Perform the main task of the thread
System.out.println("Thread is running...");
// Check for interruption and handle it
if (Thread.interrupted()) {
System.out.println("Thread interrupted. Cleaning up and stopping...");
// Cleanup code if needed
break;
}
// Simulate some work
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted while sleeping. Cleaning up and stopping...");
// Cleanup code if needed
break;
}
}
System.out.println("Thread stopped gracefully.");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
// Let the thread run for a while
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Request the thread to stop by interrupting it
thread.interrupt();
}
}
Note - Thread.stop() method forcibly terminates a thread. It is deprecated and highly discouraged to use.
Synchronizing threads
synchronized keyword can be used to synchronize blocks of code or an entire method.
Example
public class Counter {
private int count;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// Create multiple threads to increment the 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();
}
});
// Start the threads
t1.start();
t2.start();
// Wait for the threads to complete
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Get the final count
System.out.println("Count: " + counter.getCount());
}
}
The output will be Count: 2000
The join() method forces one thread to wait for the completion of another thread before proceeding further.
Inter thread communication
wait(), notify() and notifyAll() methods are used for inter-thread communication. They are defined in the Object class in Java. The methods wait(), notify(), and notifyAll() are typically called from within a synchronized context (synchronized block or synchronized method).
The wait() method causes the current thread to release the lock on the object it is called upon and enter a waiting state until another thread invokes the notify() or notifyAll() method on the same object.
The notify() method wakes up a single thread that is waiting on the object’s monitor. If multiple threads are waiting, only one of them will be awakened, and the choice is arbitrary.
The notifyAll() method wakes up all threads that are waiting on the object’s monitor. Each thread will then compete for the lock.
In the below example, the Message class represents a shared resource between a producer and a consumer thread.
The put() method is used by the producer thread to put a message into the shared resource, and the get() method is used by the consumer thread to retrieve a message.
public class Message {
private String content;
private boolean isMessageAvailable;
public synchronized void put(String message) {
while (isMessageAvailable) {
try {
wait(); // Wait until the message is consumed
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
content = message;
isMessageAvailable = true;
System.out.println("Put: " + message);
notify(); // Notify the consumer thread that the message is available
}
public synchronized String get() {
while (!isMessageAvailable) {
try {
wait(); // Wait until a message is put
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
String message = content;
isMessageAvailable = false;
System.out.println("Got: " + message);
notify(); // Notify the producer thread that the message has been consumed
return message;
}
}
The MessageOrchestrator class creates an instance of the Message class and creates two threads - producerThread and consumerThread. The producer thread puts five messages into the shared resource, with a delay of 1 second between each message. The consumer thread gets the messages from the shared resource with a similar delay.
The producer and consumer threads coordinate by using wait() and notify() to ensure that the producer puts a message only when the consumer has consumed the previous message, and vice versa.
public class MessageOrchestrator {
public static void main(String[] args) {
Message message = new Message();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String msg = "Message " + i;
message.put(msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String msg = message.get();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producerThread.start();
consumerThread.start();
}
}
The output here is straightforward. One producer thread puts a message and waits until one consumer thread consumes it.
The output looks like this
Put: Message 0
Got: Message 0
Put: Message 1
Got: Message 1
Put: Message 2
Got: Message 2
Put: Message 3
Got: Message 3
Put: Message 4
Got: Message 4
In the above example we will allow the producer to put messages without checking if the consumer consumed it. We do that by removing the below block of code from the Message Class.
while (isMessageAvailable) {
try {
wait(); // Wait until the message is consumed
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
Let’s also make sure the consumer is very slow at consuming by increasing the sleep time to 10000 (Thread.sleep(10000);) in consumer thread.
Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String msg = message.get();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
The output is below.
The consumer thread keeps waiting in the while loop of get() but sadly the producer thread has no more messages to publish.
Put: Message 0
Got: Message 0
Put: Message 1
Put: Message 2
Put: Message 3
Put: Message 4
Got: Message 4
Is there a higher-level abstraction in Java for managing threads and executing tasks? Well, yes.
I will write about ExecutorService interface in a future post. It is an interface that is commonly used to manage multithreading in Java.
It is a better alternative to directly using the Thread class as it abstracts away lot of problems that arise during thread management.