25 Java - Multithreading 2

Thread Creation Ways

  • Implementing Runnable interface

  • extending Thread class

Implementing Runnable interface

Step1: Create a Runnable Object

  • Create a class that implements Runnable interface

  • Implement 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 class

  • Override 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 StampMainTh1Th2
t1Start
t2Th1 & Th2 createdCreatedCreated
t3Start Th1 and Th2Acquired Lock on R1Sleep (1000)
t4Trying to Acquire Lock on R1
t5Suspend Th1SuspendedWaiting 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