25 Java - Multithreading 2
Thread Creation Ways
Implementing
Runnable
interfaceextending
Thread
class
Implementing Runnable
interface
Step1: Create a Runnable Object
Create a class that implements
Runnable
interfaceImplement the
run()
method to tell the task which thread has to do.
public class MultithreadingLearning implements Runnable{
@Override
public void run() {
System.out.println("Code executed by thread: "+ Thread.currentThread().getName());
}
}
Step2: Start the thread
Create an instance of class that implement
Runnable
Pass the Runnable object to the Trhead Constructor
Start the thread.
public class Main {
public static void main(String[] args) {
System.out.println("Going inside the main method: "+Thread.currentThread().getName());
MultithreadingLearning runnableObj = new MultithreadingLearning();
Thread thread = new Thread(runnableObj);
thread.start();
System.out.println("Finish the main method: "+Thread.currentThread().getName());
}
}
Going inside the main method: main
Finish the main method: main
Code executed by thread: Thread-0
extending Thread
class
Step1: Create a Thread Subclass
Create a class the extends
Thread
classOverride the
run()
method to tell the task which thread has to do.
public class MultithreadingLearning extends Thread{
@Override
public void run() {
System.out.println("Code executed by thread: "+ Thread.currentThread().getName());
}
}
Step 2: Initiate and Start the thread
Create an instance of the subclass
call the start() method to begin the execution
public class Main {
public static void main(String[] args) {
System.out.println("Going inside the main method: "+Thread.currentThread().getName());
MultithreadingLearning myThread = new MultithreadingLearning();
myThread.start();
System.out.println("Finish the main method: "+Thread.currentThread().getName());
}
}
Going inside the main method: main
Finish the main method: main
Code executed by thread: Thread-0
Why we have 2 ways to create threads?
A class can implement more than 1 interface
A class can extend only 1 class.
If we want to create a thread using extends, then we cannot inherit other classes. However, using an interface provides more flexibility, and we can also implement other interfaces as needed.
Thread Lifecycle (States)
New
Thread has been created but not started.
It's just an object in memory
Runnable
Thread is ready to run.
Waiting for CPU time.
Running
- When thread start executing its code.
Blocked
Different scenarios where runnable thread goes into the Blocking state:
I/O: like reading from a file or database.
Lock acquired: if thread want to lock on a resource which is locked by other thread, it has to wait.
Releases all the MONITOR LOCKS
Waiting
Thread goes into this state when we call the wait() method, makes it non runnable.
It goes back to runnable, once we call notify() or notifyAll() method.
Releases all the MONITOR LOCKS
Timed Waiting
Thread waits for specific period of time and comes back to runnable state, after specific conditions met like sleep(), join()
Do not Releases any MONITOR LOCKS
Terminated
- Life of thread is completed, it cannot be started back again.
Monitor Lock
It helps to make sure that only 1 thread goes inside the particular section of code (a synchronized block or method)
Example 1
public class MonitorLockExample {
public synchronized void task1(){
try{
System.out.println("inside task1");
Thread.sleep(1000);
}
catch (Exception e){
//exception handling here
}
}
public void task2(){
System.out.println("task2, but before synchronized");
synchronized (this){
System.out.println("task2, inside synchronized");
}
}
public void task3(){
System.out.println("task3");
}
}
public class Main {
public static void main(String[] args) {
MonitorLockExample obj = new MonitorLockExample();
Thread t1 = new Thread(()->{obj.task1();});
Thread t2 = new Thread(()->{obj.task2();});
Thread t3 = new Thread(()->{obj.task3();});
t1.start();
t2.start();
t3.start();
}
}
inside task1
task2, but before synchronized
task3
task2, inside synchronized
Explanation
First, the t1 thread gets triggered, followed by t2 and t3. Here’s how the execution proceeds:
t1 acquires the lock on the object obj (since it’s synchronized) and prints a statement. Then it enters sleep mode without releasing the lock.
Meanwhile, t2 and t3 execute statements that do not require locking the object obj. They proceed independently.
However, when t2 encounters a synchronized block (which requires locking obj), it waits until t1 releases the lock (due to the sleep). After 1000 milliseconds, t2 acquires the lock and executes its task.
In summary, the synchronized behavior ensures that only one thread at a time can access the synchronized block associated with the object obj
Example 2
public class SharedResource {
boolean itemAvailable = false;
//synchronized put the monitor lock
public synchronized void addItem(){
itemAvailable = true;
System.out.println("Item added by: "+ Thread.currentThread().getName());
notifyAll();
}
public synchronized void consumeItem(){
System.out.println("ConsumeItem method invoked by: "+ Thread.currentThread().getName());
//using while loop to avoid "spurious wake up", sometimes because of system noise
while (!itemAvailable){
try {
System.out.println("Thread "+ Thread.currentThread().getName()+" is waiting now");
wait();
} catch (InterruptedException e) {
//handle exception
}
}
System.out.println("Item Consumed by: "+ Thread.currentThread().getName());
itemAvailable = false;
}
}
public class ProducerTask implements Runnable{
SharedResource sharedResource;
public ProducerTask(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
System.out.println("Producer thread: "+Thread.currentThread().getName());
try{
Thread.sleep(5000l);
}catch (Exception e){
//handle Exception
}
sharedResource.addItem();
}
}
public class ConsumerTask implements Runnable{
SharedResource sharedResource;
public ConsumerTask(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
System.out.println("Consumer thread: "+Thread.currentThread().getName());
sharedResource.consumeItem();
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Main method start");
SharedResource sharedResource = new SharedResource();
//producer thread
Thread producerThread = new Thread(new ProducerTask(sharedResource));
//consumer Thread
Thread consumerThread = new Thread(new ConsumerTask(sharedResource));
//thread is in "RUNNABLE state"
producerThread.start();
consumerThread.start();
System.out.println("Main method end");
}
}
Main method start
Main method end
Producer thread: Thread-0
Consumer thread: Thread-1
ConsumeItem method invoked by: Thread-1
Thread Thread-1 is waiting now
Item added by: Thread-0
Item Consumed by: Thread-1
Explanation
Here, the main thing to observe is that the first consumer gets the initial opportunity to acquire a monitor lock on the sharedResource object. However, due to the unavailability of stock, it enters a wait state. In this wait state, the monitor lock held on the object is released.
Subsequently, when the producer invokes the addItem method, it can easily acquire the monitor lock since it has been released. The producer adds the item and then notifies all the threads that are in a wait state, waking them up.
Finally, the consumer thread acquires the lock and completes the consumption process.
Implement Producer Consumer Problem
Question:
Two threads, a producer and a consumer, share a common, fixed-size buffer as a queue.
The producer's job is to generate data and put it into the buffer, while the consumer's job is to consume the data from the buffer.
The problem is to make sure that the producer won't produce data if the buffer is full, and the consumer won't consume data if the buffer is empty.
import java.util.LinkedList;
import java.util.Queue;
public class SharedResource {
private Queue<Integer> sharedBuffer;
private int bufferSize;
public SharedResource(int bufferSize) {
sharedBuffer = new LinkedList<>();
this.bufferSize = bufferSize;
}
public synchronized void produce(int item) throws Exception{
//If Buffer is full, wait for the consumer to consume items
while (sharedBuffer.size() == bufferSize){
System.out.println("Buffer is full, Producer is waiting for consumer");
wait();
}
sharedBuffer.add(item);
System.out.println("Produced: "+item);
//Notify the consumer that there are items to consume now
notify();
}
public synchronized int consume() throws Exception {
//Buffer is empty, wait for the producer to produce items
while (sharedBuffer.isEmpty()){
System.out.println("Buffer is empty, Consumer is waiting for producer");
wait();
}
int item = sharedBuffer.poll();
System.out.println("Consumed: "+item);
// Notify the producer that there is space in the buffer now
notify();
return item;
}
}
public class ProducerConsumerLearning {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource(3);
//Creating producer thread using Lambda expression
Thread producerThread = new Thread(()->{
try{
for (int i=1; i<=6; i++){
sharedResource.produce(i);
}
}catch (Exception e){
//handle exception here
}
});
//creating consumer thread using Lambda expression
Thread consumerThread = new Thread(()->{
try{
for (int i=1; i<=6; i++){
sharedResource.consume();
}
}catch (Exception e){
//handle exception here
}
});
producerThread.start();
consumerThread.start();
}
}
Produced: 1
Produced: 2
Produced: 3
Buffer is full, Producer is waiting for consumer
Consumed: 1
Consumed: 2
Consumed: 3
Buffer is empty, Consumer is waiting for producer
Produced: 4
Produced: 5
Produced: 6
Consumed: 4
Consumed: 5
Consumed: 6
Why Stop, Resume, Suspend methods is deprecated?
STOP: Terminates the thread abruptly, No lock release, No resource clean up happens.
SUSPEND: Put the Thread on hold (suspend) for temporarily. No lock is release too.
RESUME: Used to Resume the execution of Suspended thread.
Both this operation could led to issues like deadlock.
Lets see an example of it
Th1 -> Lock R1
Th2 -> wait for R1
Th1 -> Suspend
Time Stamp | Main | Th1 | Th2 |
t1 | Start | ||
t2 | Th1 & Th2 created | Created | Created |
t3 | Start Th1 and Th2 | Acquired Lock on R1 | Sleep (1000) |
t4 | Trying to Acquire Lock on R1 | ||
t5 | Suspend Th1 | Suspended | Waiting for Lock to Release (Dead Lock) |
Dead Lock (Using Suspend)
public class SharedResource {
boolean isAvailable = false;
public synchronized void produce() {
System.out.println("Lock acquired");
isAvailable =true;
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Lock Release");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
System.out.println("Main thread started");
Thread th1 = new Thread(()->{
System.out.println("Thread1 calling produce method");
resource.produce();
});
Thread th2 = new Thread(()->{
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread2 calling produce method");
resource.produce();
});
th1.start();
th2.start();
Thread.sleep(3000);
System.out.println("thread1 is suspended");
th1.suspend();
System.out.println("Main thread is finishing its work");
}
}
Main thread started
Thread1 calling produce method
Lock acquired
Thread2 calling produce method
thread1 is suspended
Main thread is finishing its work
Solution
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
System.out.println("Main thread started");
Thread th1 = new Thread(()->{
System.out.println("Thread1 calling produce method");
resource.produce();
});
Thread th2 = new Thread(()->{
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread2 calling produce method");
resource.produce();
});
th1.start();
th2.start();
Thread.sleep(3000);
System.out.println("thread1 is suspended");
th1.suspend();
Thread.sleep(3000);
System.out.println("thread1 is resumed");
th1.resume();
System.out.println("Main thread is finishing its work");
}
}
Main thread started
Thread1 calling produce method
Lock acquired
Thread2 calling produce method
thread1 is suspended
thread1 is resumed
Main thread is finishing its work
Lock Release
Lock acquired
Lock Release
JOIN
When JOIN method is invoked on a thread object. Current thread will be blocked and waits for the specific thread to finish.
It is helpful when we want to coordinate between threads or to ensure we complete certain task before moving ahead.
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
System.out.println("Main thread started");
Thread th1 = new Thread(()->{
System.out.println("Thread1 calling produce method");
resource.produce();
});
th1.start();
System.out.println("Main thread is waiting for thread1 to finish now");
th1.join();
System.out.println("Main thread is finishing its work");
}
}
Main thread started
Main thread is waiting for thread1 to finish now
Thread1 calling produce method
Lock acquired
Lock Release
Main thread is finishing its work
Thread Priority
Priorities are integer ranging from 1 to 10.
1 -> Low Priority
10 -> Highest Priority
Even we set the thread priority while creation, its not guaranteed to follow any specific order, its just a hint to thread scheduler which to execute next. (But its not strict rule)
When new thread is created, it inherit the priority of its parent thread.
We can set custom priority using
setPriority(int priority)
method
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
System.out.println("Main thread started");
Thread th1 = new Thread(()->{
System.out.println("Thread1 calling produce method");
resource.produce();
});
th1.setPriority(5);
th1.start();
System.out.println("Main thread is finishing its work");
}
}
Daemon thread (ASYNC)
Something which is running in Asynchronous manner (ASYNC) is called Daemon
There are 2 types of threads
User Thread
Daemon Thread -
th.setDaemon(true)
Daemon thread is alive till any one user thread is alive.
If all user threads finished execution then daemon thread also stopped.
Examples
The garbage collector is a daemon thread.
Autosave in the editor: Autosave is a daemon thread. It works in the background and stops once the program is closed.
Logging is a daemon thread.
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
System.out.println("Main thread started");
Thread th1 = new Thread(()->{
System.out.println("Thread1 calling produce method");
resource.produce();
});
th1.setDaemon(trye);
th1.start();
System.out.println("Main thread is finishing its work");
}
}
Main thread started
Main thread finished its work
Thread1 Calling produce method
Lock acquired
Subscribe to my newsletter
Read articles from Chetan Datta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Chetan Datta
Chetan Datta
I'm someone deeply engrossed in the world of software developement, and I find joy in sharing my thoughts and insights on various topics. You can explore my exclusive content here, where I meticulously document all things tech-related that spark my curiosity. Stay connected for my latest discoveries and observations.