Multithreading in Java: A Detailed Overview

Multithreading in Java: A Detailed Overview

To maximize the potential of the processor, we use a concept called multitasking. By utilizing this approach, we ensure efficient utilization of CPU time and minimize idle periods.

If we go deeper into Multitasking, we will get to know that it is of two types

  • Process-based multitasking

  • Thread based MultiTasking

Process-based multitasking involves running two or more processes or programs simultaneously. For instance, you may be reading this blog on your web browser while also having a code editor running or any other application on your system. On the other hand, multitasking with threads focuses on concurrent execution within the same process. For example, you can play a game online while simultaneously engaging in a chat conversation with other players.

In essence, both approaches leverage the multitasking capabilities of the CPU. However, upon closer examination, we discover that multitasking with processes incurs higher costs and overhead compared to multitasking with threads. This is because in the case of the latter, you operate, handle communication, and do context switching (which refers to the threads taking turns to utilize CPU time) by remaining within the same process.

Now, if we talk about multithreading, it is all about executing 2 or more than 2 threads at the same time, and it is of 2 types:

  • Multithreading in a single-core environment

  • Multithreading in a Multi-core environment

In the single-core environment, all the threads have to wait their turn to get the slice of CPU. Because only one thread executes at a single time. But all of this is happening in such a small unit of time, that it seems that the execution is happening simultaneously. Whereas in the multicore environment, more than one thread executes at the same time.

Main Thread

By default, whenever we run our application or program, it is the main thread that starts the execution.

@SLF4J
public class MultiThreading {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();

        log.info("current thread: " + t);

        try {
            t.sleep(10000);
            log.info("after 10 seconds sleep");
        } catch (InterruptedException ex) {
            log.error("Thread is interrupted");
        }
    }
}

Below is the output of the above program

current thread: Thread[main,5,main]
after 10 seconds sleep

Here, 'main' which is the first element in the list, is the default name of the thread that initiates from the main method. Next is the priority which is also 5 by default. The last element is the group name of the thread, which is also main by default.

Creating the Threads:

There are two ways by which we can create threads in Java.

  • By implementing Runnable Interface

  • By extending the Thread class.

Let us look at how we can use the Runnable interface to create the threads in our application.

1) Runnable Interface

@SLF4J
public class ImplementingRunable implements Runnable {
    Thread t;

    ImplementingRunable() {
        log.info("initializing");
        t = new Thread(this, "new thread");
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i <= 5; i++) {
                log.info("executing child thread: " + t);
                t.sleep(700);
            }
        } catch (InterruptedException ex) {
            log.error("thread interupted");
        }

        log.info("exiting child thraed");
    }


    public static void main(String[] args) throws InterruptedException {
        ImplementingRunable ob = new ImplementingRunable();
        ob.t.start();


        for (int i = 0; i <= 5; i++) {
            log.info("main thread");
            Thread.sleep(1000);
        }
        log.info("exiting main thread");
    }
}

To create the thread, we need to implement the Runnable as you can see above and have to provide implementation to its run method to provide the point of execution for this thread.

We then declare a reference to the Thread class and initialize it using the below statement:

t = new Thread(this, "new thread");

The first argument here is the point of execution which is the current instance in this case, and the second argument is the name of the thread we want to give.

Finally, to start the execution of this thread, we need to call the start method on its thread reference.

2) Thread Class

Now let us see how we can create the threads by extending the thread class:

@SLF4J
public class ExtendingThreadClass extends Thread {

    public void run() {

        try {
            for (int i = 0; i <= 5; i++) {
                lof.info("child thread executing: " +             Thread.currentThread());
                Thread.sleep(750);
            }

        } catch (InterruptedException ex) {
            log.error("thread interupted");
        }


    }

    public static void main(String[] args) {
        ExtendingThreadClass ob = new ExtendingThreadClass();

        ob.start();

        try {
            for (int i = 0; i <= 5; i++) {
                log.info("main thread");
                Thread.sleep(1000);
            }
        } catch (InterruptedException ex) {
            log.error("main thread interrupted");
        }
    }
}

The only difference is that we need to extend the Thread class which makes it the parent type of extending class. Now we just need to provide the implementation to the run method like we were doing previously, to provide the point of execution to the thread. Finally, we have to start the thread by calling the start method on it.

Some PROS and CONS

The only limitation in the latter approach is that we can not extend any other class if we are already extending the Thread class for creating the thread. As Java does not support multiple inheritance, of course, we can make use of multiple interfaces but that is not what we are discussing at the moment.

Synchronization

Synchronization is the process of making sure that there are no inconsistencies with the data when 2 or more threads are accessing the same resource. With the help of synchronization, we avoid the race condition where multiple threads try to get a hold of the resource at the same time.

The concept of monitor plays an important role in synchronization. Monitor is a kind of lock that is given to a thread that has control of the resource. Only one thread can have the monitor on any object at a given time. This means that when one thread enters the monitor on an instance, all other threads go into the waiting state.

There are 2 ways to achieve synchronization, let us have a look at both below:

1) Synchronized Methods

The first option is to use the synchronized keyword with the method which you want to be thread-safe. When a thread enters the synchronized method on an instance, it acquires the monitor on that instance, therefore all other threads have to wait for it to give control of the monitor.

@Slf4j
public class SynchronizedMethod {

    synchronized void display() {
        try {
            log.info("Executing " + Thread.currentThread().getName());
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            log.error("Thread Interrupted");
        }
    }

    public static void main(String[] args) {

        SynchronizedMethod obj = new SynchronizedMethod();

        Runnable runnable = () -> {
            obj.display();
        };

        Thread thread1 = new Thread(runnable, "first thread");
        Thread thread2 = new Thread(runnable, "second thread");

        thread1.start();
        thread2.start();

    }
}

Please note that we have used a different way to initialize and start the threads. This is another way where you can first define a runnable implementation using the lambda construct and then pass it as the point of execution to the thread instance.

In the above sample code, any thread that acquires the lock on the synchronized method will print a statement on the console and then will sleep for 5 seconds, in the meantime the other thread will have to wait so that the running thread finishes its execution and releases the lock on the instance.

2) Synchronized Block

A synchronized block is an alternative to the synchronized methods. We can use them when the methods or other resources of a class are not synchronized. This is useful in cases when you are using the code of a third-party library or any legacy code.

Similar to synchronized block, when a thread enters the synchronized block on an instance it acquires the lock on that instance. One thing to note here is that any other code block which is not synchronized can be accessed at the same time on that instance.

Below is the modified version of the synchronized methods example.

@Slf4j
@Slf4j
public class SynchronizedBlock {

    void display() {
        try {
            log.info("Executing " + Thread.currentThread().getName());
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            log.error("Thread Interrupted");
        }
    }

    public static void main(String[] args) {

        SynchronizedBlock obj = new SynchronizedBlock();

        Runnable runnable = () -> {
            synchronized (obj){
                obj.display();
            }
        };

        Thread thread1 = new Thread(runnable, "first thread");
        Thread thread2 = new Thread(runnable, "second thread");

        thread1.start();
        thread2.start();

    }
}

Some Useful Thread Methods

Below are some useful methods provided by the Thread class

run

Point of execution.

start

Start the execution of the thread.

getPriority

Returns the current thread priority.

getName

Returns the current thread's name.

getState

Returns the state of the thread

isAlive

To check if the current thread is alive

sleep

Suspends the current thread for the defined period.

There is some state of a thread at any given moment, you should have an idea bout it such as 'waiting', 'terminated', and 'runnable'. You can use the getState method on any thread to check its current state.

Other than this there are few methods provided by the Object class for the efficient management of a thread.

wait

Suspends the calling thread to a defined or undefined period until notified to resume.

notify

when called, the thread gets resumed that called wait.

notifyAll

all the threads that called wait on this instance are resumed.

We have seen how to achieve synchronization but it is not enough. There can be many cases where when we want to have an order maintained in a multithreaded environment. For this purpose, we have the above methods provided to us by the Object class to have communication between the threads possible.

Let us look at the below example of how we can make sure of an orderly execution of multiple threads with the use of wait() and notify().

@Slf4j
class Message {
    private String message;
    private boolean isConsumed = true;

    synchronized void setMessage(String msg) {
        while (!isConsumed) {
            try {
                wait();
            } catch (InterruptedException ex) {
                log.error("thread interrupted {}", ex);
            }
        }
        message = msg;
        isConsumed = false;
        notify();
    }

    synchronized String getMessage() {
        while (isConsumed) {
            try {
                wait();
            } catch (InterruptedException ex) {
                log.error("thread interrupted {}", ex);
            }
        }

        isConsumed = true;
        notify();
        return message;
    }


}

@Slf4j
class Producer implements Runnable {
    private Message message;

    Producer(Message message) {
        this.message = message;
    }


    @Override
    public void run() {

        String[] messages = {"message1", "message2", "message3"};
        for (String msg : messages) {
            message.setMessage(msg);
            log.info("message produced {}", msg);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                log.error("thread interrupted {}", ex);
            }
        }
        message.setMessage("exit");
    }
}

@Slf4j
class Consumer implements Runnable {
    Message message;
    String msg;

    Consumer(Message message) {
        this.message = message;
    }


    @Override
    public void run() {
        while (!(msg = message.getMessage()).equals("exit")) {
            log.info("consumed message {}", msg);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                log.error("thread interrupted {} ", ex);
            }
        }

    }
}


public class ThreadCommunication {
    public static void main(String[] args) {
        Message instance = new Message();
        Thread producerThread = new Thread(new Producer(instance));
        Thread consumerThread = new Thread(new Consumer(instance));

        producerThread.start();
        consumerThread.start();
    }

}

We have 2 methods in the Message class, the setMessage method looks for the variable 'isConsumed', if true then it sets the message to its new value otherwise it waits until the notify method is called.

Similarly, the getMessage looks if the message is not consumed it sets the 'isConsumed' to true, returns the new value, and also calls the notify method to resume the setMessage thread. Otherwise, if the message is already consumed it waits for the notify method to be called.

One thing to note here is that we are using the while loop to validate the condition, this is because, in the multithreaded environment, we have the term 'spurious invocation' for the sleeping thread under which, the thread which is on wait gets invoked on a false flag. To avoid this it is common practice to use the while loops.

Deadlock

One thing that you need to avoid in a multithreaded environment is the deadlock scenario. This occurs when two threads get into a dependency on each other. Let us say we have 2 threads 'thread1' and 'thread2' having locks on 2 separate instances, now both of them are waiting to get the lock on each other's instance. This can only be possible when one of them let's say 'thread1' releases the current lock, which will never happen in this case. As for this to be achieved, it has to acquire the lock of the 'thread2' monitor.

Below is the example scenario

import lombok.extern.slf4j.Slf4j;

@Slf4j
class TestClass {
    synchronized void method(AnotherTestClass anotherTestClass) {
        log.info("Executing " + Thread.currentThread().getName() + " in TestClass");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            log.error("thread interrupted");
        }

        log.info("calling the method of AnotherTestClass");
        anotherTestClass.method(this);
    }
}

@Slf4j
class AnotherTestClass {
    synchronized void method(TestClass testClass) {
        log.info("Executing " + Thread.currentThread().getName() + " in AnotherTestClass");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            log.error("thread is interrupted");
        }

        log.info("Calling the TestClass method");
        testClass.method(this);
    }
}


public class DeadLock {

    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        AnotherTestClass anotherTestClass = new AnotherTestClass();

        Runnable r1 = () -> {
            testClass.method(anotherTestClass);
        };

        Runnable r2 = () -> {
            anotherTestClass.method(testClass);
        };

        Thread t1 = new Thread(r1, "first thread");
        Thread t2 = new Thread(r2, "second thread");

        t1.start();
        t2.start();
    }
}

This is all on multithreading in Java. For more, I will suggest reading 'Java the Complete Reference' as it gives a comprehensive guide on any given topic.