Multi-Threading in Java

Overview

  • Introduction
    • Multitasking: Executing several tasks simultaneously is the concept of Multitasking. There are two types of Multitasking
      • Process Based Multitasking: Executing several tasks simultaneously where each task is a separate independent program (process) is called Process Based Multitasking. e.g.
        • While typing a Java program in the editor we can listen audio songs from the same system. At the same time, we can download a file from net. All these tasks will be executed simultaneously and independent of each other. Hence it is Process Based Multitasking. Process Based Multitasking is best suitable at OS level.
      • Thread Based Multitasking: Executing several tasks simultaneously where each task is a separate independent part of the same program is called Thread Based Multitasking. And each independent part is called a Thread. Thread Based Multitasking is best suitable at programmatic level.
    • Whether it is Process Based or Thread Based, the main objective of Multitasking is to reduce response time of the system and to improve performance.
    • The main important application areas of Multi-Threading are
      • To develop multimedia graphics.
      • To develop animations
      • To develop video games.
      • To develop web servers and application servers etc.
    • When compared with old languages developing Multi-Threaded applications in Java is very easy because Java provides inbuilt support for Multi-Threading with rich API [Thread, Runnable, Thread Group...]
  • Defining a Thread: We can define a thread in the following two ways:
    • By extending a Thread class.
 public class MyThread extends Thread {
@Override
public void run() {
/* This for loop is executed by Child Thread */
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
 public class ThreadMain {
public static void main(String[] args) {
/* This is Thread Instantiation */
MyThread thread = new MyThread();
thread.start();
/* This for loop is executed by Main Thread */
for (int i = 0; i < 10; i++) {
System.out.println("Main Thread");
}
}
}
      • Case 1 (Thread Scheduler): 
        • It is the part of JVM.
        • It is responsible to schedule Threads i.e. if multiple Threads are waiting to get the chance of execution, then in which Threads will be executed is decided by Thread Scheduler.
        • We can't expect exact algorithm followed by Thread Scheduler. It is varied from JVM to JVM. Hence, we can't expect Thread execution order and exact output.
        • Hence whenever situation comes to Multi-Threading there is no guarantee for exact output but we can provide several possible outputs.
        • The following are the various possible outputs for the following program.
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    Main Thread
    0
1
2
3
4
5
6
7
8
9
      • Case 2 (Difference between start() and run()):
        • In the case of start() a new Thread will be created which is responsible for the execution of run().
        • But in the case of run() a new Thread won't be created and run() will be executed just like a normal method call by Main Thread.
        • Hence in the following program if we replace start() with run() then the output is
 public class ThreadMain {
public static void main(String[] args) {
/* This is Thread Instantiation */
MyThread thread = new MyThread();
thread.run();
/* This for loop is executed by Main Thread */
for (int i = 0; i < 10; i++) {
System.out.println("Main Thread");
}
}
}
 public class MyThread extends Thread {
@Override
public void run() {
/* This for loop is executed by Child Thread */
for (int i = 0; i < 10; i++) {
System.out.println("Child Thread");
}
}
}
 Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
        • This total output is produced by Main Thread.
      • Case 3 (Importance of Thread class start()):
        • Thread class start() is responsible to register the Thread with Thread Scheduler and all other mandatory activities. Hence without Thread class start(), there is no chance of starting a new Thread in Java. Due to this, Thread class start() is considered as 'Heart' of Multi-Threading. Task of Thread class start()
          • Register this Thread with Thread Scheduler.
          • Perform all other mandatory activities.
          • Invoke run()
      • Case 4 (Overloading of run()):
        • Overloading of run() is always possible but Thread class start() can invoke only no-argument run(). The other overloaded methods, we have to call explicitly, like a normal method call.
 public class OverloadingRunThread extends Thread {
@Override
public void run() {
System.out.println("No Argument Run Method");
}

public void run(int i) {
System.out.println("Int Argument Run Method");
}
}
 public class OverloadingRunMain {
public static void main(String[] args) {
OverloadingRunThread thread = new OverloadingRunThread();
thread.start();
}
}
 No Argument Run Method
        • Case 5 (If we are not Overriding run()):
          • If we are not overriding run() then Thread class run() will be executed which has empty implementation. Hence we won't get any output.
 public class NotOverridingRunThread extends Thread {
}
 public class NotOverridingRunMain {
public static void main(String[] args) {
NotOverridingRunThread thread = new NotOverridingRunThread();
thread.start();
}
}
          • It is highly recommended to override run(), otherwise don't go for Multi-Threading concept.
        • Case 6 (Overriding of start()):
          • If we override start() then our start() will be executed just like a normal method call and new Thread won't be created.
 public class OverrideStartThread extends Thread {
@Override
public synchronized void start() {
System.out.println("Start Method");
}

@Override
public void run() {
System.out.println("Run Method");
}
}
 public class OverrideStartMain {
public static void main(String[] args) {
OverrideStartThread thread = new OverrideStartThread();
thread.start();
System.out.println("Main Method");
}
}
 Start Method
Main Method
          • As you can see in the above output, print command of run() is not getting executed. Hence it is proved that new Thread is not created.
          • It is not recommended to override Thread class start() otherwise, don't go for Multi-Threading concept.
        • Case 7 (Overriding of start() but calling super.start() above other commands):
          • In the following program, Child Thread is responsible to print "Run Method" and Main Thread is responsible to print "Start Method" and "Main Method".
 public class OverrideStartThread extends Thread {
@Override
public synchronized void start() {
super.start();
System.out.println("Start Method");
}

@Override
public void run() {
System.out.println("Run Method");
}
}
 public class OverrideStartMain {
public static void main(String[] args) {
OverrideStartThread thread = new OverrideStartThread();
thread.start();
System.out.println("Main Method");
}
}
 Start Method
Main Method
Run Method
          • There are other possible outputs too of the above program.
        • Case 8 (Thread Lifecycle):
        • Case 9 :
          • After starting a Thread if we are trying to restart the same Thread then we will get runtime exception saying IllegalThreadStateException.
 public class OverrideStartMain {
public static void main(String[] args) {
OverrideStartThread thread = new OverrideStartThread();
thread.start();
System.out.println("Main Method");
thread.start();
}
}
    • By implementing a Runnable interface.
      • We can define a Thread by implementing a Runnable Interface. Runnable Interface present java.lang package and it contains only one method i.e. run() [public void run()].
 public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " Thread");
}
}
}
 public class RunnableInterfaceMain {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " Thread");
}
}
}
 Thread-0 Thread
Thread-0 Thread
Thread-0 Thread
Thread-0 Thread
Thread-0 Thread
main Thread
main Thread
main Thread
main Thread
main Thread
      • We will get mixed output and we can't tell exact output.
      • Case Study:
 MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread();
Thread thread = new Thread(runnable);
        • Case 1: thread1.start() - A new Thread will be created and which is responsible for the execution of Thread class run(), which has empty implementation.
        • Case 2: thread1.run() - No new Thread will be created and Thread class run() will be executed just like a normal method call.
        • Case 3: thread.start() - A new Thread will be created which is responsible for the execution of my Runnable class run().
        • Case 4: thread.run() - A new Thread won't be created and my Runnable run() will be executed just like a normal method call.
        • Case 5: runnable.start() - We will get compile time error saying "Cannot find symbol."
        • Case 6: runnable.run() - No new Thread will be created and MyRunnable run() will be executed like normal method call.
    • Which approach is best to define a thread?
      • Among two ways of defining a Thread, implements Runnable approach is recommended.
      • In the first approach our class always extends Thread class, there is no chance of extending any other class. Hence we are missing Inheritance benefits.
      • But in the second approach while implementing Runnable interface, we can extend any other class. Hence we won't miss any Inheritance benefit.
      • Because of above reason implementing Runnable Interface approach is recommended then extending Thread class.
    • Thread Class Constructors:
      • Thread t = new Thread ();
      • Thread t = new Thread (Runnable r);
      • Thread t = new Thread ("String name");
      • Thread t = new Thread (Runnable r, "String name");
      • Thread t = new Thread (ThreadGroup g, "String name");
      • Thread t = new Thread (ThreadGroup g, Runnable r);
      • Thread t = new Thread (ThreadGroup g, Runnable r, "String name");
      • Thread t = new Thread (ThreadGroup g, Runnable r, "String name", long StackSize);
      • Durga's Approach to define a Thread (Not recommended to use): 
 public class DurgaApproachThread extends Thread{
@Override
public void run() {
System.out.println("Child Method");
}
}
 public class DurgaApproachMain {
public static void main(String[] args) {
DurgaApproachThread durgaApproachThread = new DurgaApproachThread();
Thread thread = new Thread(durgaApproachThread);
thread.start();
System.out.println("Main Thread");
}
}
 Main Thread
Child Method
  • Getting and Setting name of a Thread:
    • Every Thread in Java has some name, it may be default name generated by JVM or customized name provided by programmer.
    • We can get and set name of a Thread by using the following methods of Thread class.
 public final String getName()
 public final synchronized void setName(String name)
 public class ThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
/* This is Thread Instantiation */
MyThread thread = new MyThread();
thread.start();
System.out.println(thread.getName());
Thread.currentThread().setName("ThreadMain Thread");
/* This for loop is executed by Main Thread */
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
 main
Thread-0
ThreadMain Thread
ThreadMain Thread
ThreadMain Thread
ThreadMain Thread
ThreadMain Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
    • We can get current executing Thread Object by using Thread.currentThread().
  • Thread Priorities
    • Every Thread in Java has some priority. It may be default priority generated by JVM or customized priority provided by programmer.
    • The valid range of Thread priorities is '1 to 10'. Where is 1 is minimum priority and 10 is maximum priority.
    • Thread class defines the following constants to represent some standard priorities.
 public static final int MIN_PRIORITY = 1;
 public static final int NORM_PRIORITY = 5;
 public static final int MAX_PRIORITY = 10;
    • Thread scheduler will use priorities while allocating processor. The Thread which is having highest priority will get the chance first.
    • If two Threads having same priority then we can't expect exact execution order. It depends on Thread scheduler.
    • Thread class defines the following methods to get and set priority of a Thread.
 public final void setPriority(int newPriority)
 public final int getPriority()
    • Allowed values range 1 to 10. Otherwise we will get runtime exception: IllegalArgumentException.
    • Default Priority: The default priority only for the Main Thread is 5 but for all remaining Threads default priority will be inherited from Parent to Child i.e. whatever priority Parent class has the same priority will be there for the Child Thread.
 public class PrioritiesThread extends Thread {
@Override
public void run() {
System.out.println("Child Thread");
}
}
 public class PrioritiesMain {
public static void main(String[] args) {
System.out.println("Priority of Main Thread: " + Thread.currentThread().getPriority());
// Thread.currentThread().setPriority(11); // Throws Runtime Exception.
PrioritiesThread thread = new PrioritiesThread();
thread.setPriority(
10);
thread.start();
System.out.println("Main Thread");
}
}
 Priority of Main Thread: 5
Main Thread
Child Thread
    • If we are not setting the priority of Child Thread then both Main and Child Threads have the same priority 5 and hence, we can't expect execution order and exact output.
    • If we are not setting the priority of Child Thread then Main Thread has a priority 5 and Child Thread has a priority of 10, hence Child Thread will get the chance first followed by Main Thread (This is the expected output but real time output is different, reason is given below).
    • Note: Some platforms won't provide proper support for Thread Priorities.
  • The methods to prevent Thread execution (temporarily).
    • We can prevent a Thread execution by using the following methods:
      • yield(): 
        • yield() causes to pause current executing Thread to give the chance for waiting Threads of same priority. 
        • If there is no waiting Thread, or all waiting Threads have low priority then, same Thread can continue its execution.
        • If multiple Threads are waiting with same priority then which waiting Thread will get the chance, we can't expect, it depends on Thread scheduler.
        • The Thread which is yielded, when it will get the chance once again it depends on Thread scheduler and we can't exactly.
        • Prototype of yield():
 public static native void yield()
        • Illustration on yield()
 public class YieldMethodThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Child Thread");
Thread.yield();
}
}
}
 public class YieldMethodMain {
public static void main(String[] args) {
YieldMethodThread thread = new YieldMethodThread();
thread.start();
for (int i = 0; i < 5; i++)
System.out.println("Main Thread");
}
}
 Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
        • If we are not calling the yield() then, both Threads will be executed simultaneously and we can't expect which Thread will complete first.
        • If we are not calling the yield() then, Child Thread always calls yield() because of that Main Thread will get chance more number of times and the chance of completing Main Thread first is high.
        • Note: Some platforms won't provide proper support for yield().
      • join()
        • If a Thread wants to wait until completing some other Thread then we should go for join().
        • For e.g. if a Thread t1 wants to wait until completing t2 then t1 has to call t2.join().
        • If t1 executes t2.join() then immediately t1 will be entered into Waiting state until t2 completes.
        • Once t2 completes then t1 can continue it's execution.
        • Wedding Cards Printing Thread (t2) has to wait until Venue Fixing Thread (t1) is completed hence t2 has to call t1.join().
        • Wedding Cards Distribution Thread (t3) has to wait until Wedding Cards Printing Thread (t2) is completed hence t3 has to call t2.join().
        • Prototype of join()
          • public final void join () throws InterruptedException
          • public final void join (long milliseconds) throws InterruptedException
          • public final void join (long milliseconds, int nanoseconds) throws InterruptedException
        • Every join() throws InterruptedException which is Checked Exception, hence compulsory we should handle this exception either by using try-catch or by throws keyword, otherwise we will get compile time error.
        • Below is the Lifecycle of Thread with join()
        • Case 1 (Waiting of Main Thread until Child Thread is completed):
 public class JoinMethodThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Child Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
 public class JoinMethodMain {
public static void main(String[] args) {
JoinMethodThread thread1 = new JoinMethodThread();
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("Main Thread");
}
}
}
 Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
          • If we comment line 1 then, both Main and Child thread will be executed simultaneously and we can't expect exact output.
          • If Main Thread calls join() on Child Thread object, hence Main Thread will wait until Child Thread is completed. In this case we have the above output.
        • Case 2 (Waiting of Child Thread until Main Thread is completed):
 public class JoinMethodChildWaitingThread extends Thread {
static Thread mt;

@Override
public void run() {
try {
mt.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("Child Thread");
}
}
}
 public class JoinMethodChildWaitingMain {
public static void main(String[] args) {
JoinMethodChildWaitingThread.mt = Thread.currentThread();
JoinMethodChildWaitingThread thread = new JoinMethodChildWaitingThread();
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("Main Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
 Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Child Thread
Child Thread
Child Thread
Child Thread
          • In the above program, Child Thread calls join() on Main Thread object hence, Child Thread has to wait until Main Thread is completed. In this case, we got the above output.
        • Case 3:
          • If Main Thread calls join() on Child Thread object and Child Thread calls join() on Main Thread object then both Threads will wait forever and the program will stuck (This is something like Deadlock).
 public class JoinMethodChildWaitingThread extends Thread {
static Thread mt;

@Override
public void run() {
try {
mt.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("Child Thread");
}
}
}
 public class JoinMethodChildWaitingMain {
public static void main(String[] args) throws InterruptedException {
JoinMethodChildWaitingThread.mt = Thread.currentThread();
JoinMethodChildWaitingThread thread = new JoinMethodChildWaitingThread();
thread.start();
thread.join();
for (int i = 0; i < 5; i++) {
System.out.println("Main Thread");
Thread.sleep(1000);
}
}
}
        • Case 4 :
          • If a Thread calls join() on the same Thread itself then, the program will be stuck (This is something like Deadlock). In this case Thread has to wait infinite amount of time.
 public class JoinMethodMainDeadlockMain {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().join();
}
}
      • sleep()
        • If a Thread don't want to perform any operation for a particular amount of time then, we should go for sleep().
        • Prototype of sleep()
          • public static native void sleep (long milliseconds) throws InterruptedException 
          • public static void sleep (long milliseconds, int nanoseconds) throws InterruptedException
          • Note: Every sleep() throws InterruptedException, which is Checked Exception hence, whenever we are using sleep() compulsory we should handle InterruptedException either by try-catch or by throws keyword otherwise, we will get compile time error.
        • Below is the Lifecycle of Thread with sleep()
        • Below is an example of sleep()
 public class SleepMethodMain {
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
System.out.println("Slide - " + i);
Thread.sleep(5000);
}
}
}
 Slide - 1
Slide - 2
Slide - 3
Slide - 4
Slide - 5
      • How a Thread can interrupt another Thread?
        • A Thread can interrupt a sleeping Thread or waiting Thread by using interrupt method of Thread class. 
        • Protype of interrupt
          • public void interrupt()
        • Below is the example of interrupt()
 public class InterruptMethodThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("I am lazy Thread");
Thread.sleep(2000);
}
} catch (InterruptedException e) {
System.out.println(currentThread().getName() + " is interrupted");
}
}
}
 public class InterruptMethodMain {
public static void main(String[] args) {
InterruptMethodThread thread = new InterruptMethodThread();
thread.start();
thread.interrupt();
System.out.println("End of Main");
}
}
 End of Main
I am lazy Thread
Thread-0 is interrupted
        • If Main Thread won't call interrupt() Child Thread then, Child Thread will execute for loop 5 times.
        • If Main Thread calls interrupt() Child Thread then, we will get the above output.
      • Whenever we are calling interrupt(), if the target Thread is not in sleeping state or waiting state then there is no impact of interrupt call immediately. Interrupt call will be waited until target Thread entered into sleeping or waiting state.
      • If the target Thread entered into sleeping or waiting state, then immediately interrupt call will interrupt the target Thread.
 public class InterruptMethodMain {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
for (int i = 0; i < 5; i++) {
if (i == 2)
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " is printing " + i);
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted");
}
};

Thread thread1 = new Thread(runnable);
thread1.start();
thread1.interrupt();
}
}
 Thread-1 is printing 0
Thread-1 is printing 1
Thread-1 is interrupted
      • If the target Thread never entered into sleeping or waiting state in it's lifetime, then there is no impact of interrupt call. This is the only case where interrupt call will be wasted.
 public class InterruptMethodMain {
public static void main(String[] args) throws InterruptedException {
InterruptMethodThread thread = new InterruptMethodThread();
thread.start();
thread.interrupt();
System.out.println("End of Main");

Runnable runnable = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is printing " + i);
}
};

Thread thread1 = new Thread(runnable);
thread1.start();
thread1.interrupt();
}
}
 Thread-1 is printing 0
Thread-1 is printing 1
Thread-1 is printing 2
Thread-1 is printing 3
Thread-1 is printing 4
      • In the above example, interrupt call waited until Child Thread completes for loop 5 times.
    • Comparison table of yield(), join() and sleep()
  • Synchronization
    • synchronized is a modifier applicable only for methods and blocks but not for classes and variables.
    • If multiple Threads are trying to operate simultaneously and same Java object then there may be a chance of data inconsistency problem.
    • To overcome this problem we should go for synchronized keyword.
    • If a method or block declared as synchronized then at a time only one Thread is allowed to execute that method or block on the given Object so that data inconsistency problem will be resolved.
    • The main advantages of synchronized keyword is we can resolve data inconsistency problems but the main disadvantage of synchronized keyword is it increases waiting time of Threads and creates performance problems. Hence, if there is no specific requirement then it is not recommended to use synchronized keyword.
    • Internally synchronization concept is implemented by using lock. Every Object in Java has a unique lock. Whenever we are using synchronized keyword then only lock concept will come into the picture. 
    • If a Thread wants to execute synchronized method on the given Object first it has to get lock of that Object. Once Thread got the lock then, it is allowed to execute any synchronized method on that object.
    • Once method execution completes, automatically Thread releases lock. 
    • Acquiring and releasing locks internally takes care by JVM and programmer not responsible for this activity.
    • While a Thread executing synchronized method and given object the remaining Threads are not allowed to execute any synchronized method simultaneously on the same Object. But remaining Threads are allowed to execute non-synchronized methods simultaneously.
    • Lock concept is implemented based on Object but not based on Method. 


    • Hello World
  • Inter-Thread Communication.
    • wait()
    • notify()
    • notifyAll()
  • Deadlock
  • Daemon Threads
  • Multi-Threading Enhancements

Difference

  1. When you extend a Thread class, then you won't be able to extend other classes because Java doesn't allow Multiple Inheritance. But when you implement a Runnable Interface and if you want to extend another class, then you are allowed to do so.
  2. When you extend a Thread class, then you can use other features of Thread like yield(), interrupt() etc. But when you implement a Runnable Interface then these features are not available.
  3. You can share the object of a class that is implementing Runnable interface with other threads.

Do's and Dont's

  • You can't start a Thread which is already running otherwise you will get a Runtime Exception.
  • Runnable Interface don't have a start(). So if you want to 

Reference

Comments