Advanced
Data Structures Algorithms & System Design(HLD+LLD)
by Logicmojo

Top tech companies experts provide live online training

Learn Data Structures, Algorithms & System Design

Online live classes from 4 to 7 months programs

Get job assistance after course completion

Download Course Brochure

Back to home
Logicmojo - Updated Nov 20, 2022



Introduction

Sequential programming is a concept that all programmers are familiar with. You've undoubtedly built a program that sorts a list of names, prints "Hello World!" or calculates a list of prime numbers. These programs are sequential. In other words, each has a start, a flow, and a finish. There is only one point of execution at any given time while the program is running.
We can define a "Thread" as something that is similar to this sequential programming. This thread also has a start, flow of execution and a stop.There is only one point of execution at any given time while the thread is running. But, A thread cannot run on its own since it is not a programme in/of itself. Instead, it operates inside a programme.

Let us explore further on what a thread actually is.

Definition of a thread

"Thread is the smallest unit of any process which works independent of other units of the process. It can be defined as a subprocess which is very lightweight."

How to create a Thread in Java

In Java, a Thread is created in 2 ways:

      ⮞ By Extending the Thread Class

      ⮞ By Using the Runnable Interface

We'll look into these two ways in detail.

Thread Class

Java offers a Thread class, in the Java.lang package, with a variety of method calls to control the behaviour of threads.This class contains different constructors and methods for this purpose.
To acquire a fundamental understanding of how a thread is generated using the Thread class, look into the pseudocode below.

// Creating a thread By Extending Thread class

class createThread extends Thread {

	//Overriding the run()
	public void run()
	{
		System.out.println("A thread  is created using the Thread Class.");
	}

	public static void main(String[] args)
	{

		// Creating object of our thread class inside the main()
		createThread Thread = new createThread();
	
		// Start the thread
		Thread.start();
	}
}

Output:

A thread is created using the Thread Class.

This class offers a huge number of constructors and methods to control the working of a thread.

Some of the constructors are

      1. Thread()

      2. Thread(Runnable target)

      3. Thread(Runnable target, String Name)

      4. Thread(String name)

      5. Thread(ThreadGroup group, Runnable target)

      6. Thread(ThreadGroup group, Runnable target, String name

      7. Thread(ThreadGroup group, Runnable target, String name, long SizeofStack)

      8. Thread(ThreadGroup group, String name)

All of the above constructor are used to allocate a new object.

Some of the methods are

      1. start()- Enables the Java Virtual Machine to invoke the current thread's run method, starting the thread's execution.

      2. run()- This method does nothing and returns unless the current thread was created using a separate Runnable run object, in which case that Runnable object's run method is invoked.

      3. setName(String name)- Sets the name of the current thread to the specified string in the argument.

      4. interrupt()- The Thread is interrupted

      5. interrupted()- tests if the current thread has been interrupted.

      6. isAlive()- Checks if the current thread is alive.

      7. currentThread()- Returns the reference to the current thread.

      8. getId(), getName(), getPriority(), getState(), getThreadGroup()- Gets the (ID, Name, Priority, State, The group to which it belongs) of the current thread, respectively.

Runnable Interface

A class whose instances are intended to be executed by a thread must implement the java.lang.Runnable interface. Subclassing a Thread is not necessary when a task can be completed just by overriding the run() method of a Runnable.

How to create a new thread using runnable?

1. Implement the run() method by creating a runnable implementer.
2. Pass the implementer to the Thread after instantiating the Thread class.
3. Invoke start() of Thread instance, start internally calls run() of the implementer. Invoking start() creates a new Thread that executes the code written in run().

public class ImplementRunnable {

	public static void main(String[] args)
	{
		System.out.println("The current Thread being implemented is : "
						+ Thread.currentThread().getName());
Thread t = new Thread(new ImplementRunnable().new ImplementRunnable1());
		t.start();
	}

	private class ImplementRunnable1 implements Runnable {

		public void run()
		{
			System.out.println("Run() is being executed by " + Thread.currentThread().getName()
							);
		}
	}
}

Output:

The current Thread being implemented is : main
Run() is being executed by Thread-0

Extending Thread Class Vs Implementing Runnable Interface

1. Extending the Thread Class Enables the creating of a unique object by each thread. Whereas, Implementation of the Runnable interface facilitates sharing the same object to various threads.

2. Extending the Thread Class disables the extending of any other class if required. But implementation of Runnable facilitates saving up of space for the extension of the any other class from the current class in future or at present.





Learn More

Lifecycle of a thread

In Java, a thread undergoes 3 basic states and 3 substates in its lifecycle. At any instance of time, the state of the thread can be any one of the following only.

      Stage 1 : New

      A new thread is in the NEW state when it is created. When the thread is in this state, it indicates that it is yet to begin the execution. A thread's code has not yet been run and has not begun to execute when it is in the new state.

      Stage 2 : Runnable

      A thread is moved to the RUNNABLE state when it is ready to run. This state has two substates namely, Running or Ready to run. In the former state the thread is already in the running mode. But in the latter, the thread is yet to run at any instance of time. This responsibility of alloting the time to the thread is handled by the thread scheduler

      Stage 3 : Terminated

      A thread is in the the TERMINATED state if it has finished the expected execution or if it was abnormally stopped. This state is normally referred to as the dead thread state. The ways to kill a java thread are by using a flag or by interrupting a thread.

      Sub-Stage 1 : Blocked

      When a thread is not currently permitted to run, it is in the BLOCKED state. Attempt to access a part of code that is locked by another thread while waiting for a monitor lock, leads to the thread entering this state.

      Sub-Stage 2 : Waiting

      When a thread is waiting for another thread to carry out a certain action, it is in the WAITING state. Any thread can reach this state by invoking one of the three methods listed below: Thread.join(), object.wait(), or LockSupport.park()

      Sub-Stage 3 : Timed Waiting

      When a thread is waiting for another thread to complete a specific task within a given amount of time, it is in the TIMED WAITING state. This state can be invoked in five ways: wait(int timeout) or wait(int timeout, int nanos), thread.join(long millis), thread.sleep(long millis), LockSupport.parkNanos & LockSupport.parkUntil

Multithreading

The word multithreading means many threads processed at the same time. Java allows the parallel processing of more than one thread at a given instance of time. Both Multithreading and Mutliprocessing are used to multitask. But, Multithreading is preferred over multitasking as thread work in a shared memory. Allocation of separate memory is not done which leads to saving of space and in comparison with multiprocessing, multithreading takes less time for context-switching among the threads.
Note: Threads share a same address and the communication cost is low.

Advantages of Multithreading

1. Threads are independent. Hence, they prevent the user from getting blocked. Multiple Operations can be performed at one instance of time.
2. Multithreading helps save time.
3. If an exception occurs in one thread, it doesn't cause disturbance in the other threads.
4. CPU is utilized to its optimum.

The following code snippet illustrates this concept.

public class MultiThread implements Runnable
{
       public static void main(String[] args) {
        Thread Thread1 = new Thread("FirstThread");
        Thread Thread2 = new Thread("SecondThread");
        Thread1.start();
        Thread2.start();
        System.out.println(Thread1.getName()+ " is being Executed.");
        System.out.println(Thread2.getName()+ " is being Executed.");
    }
    @Override
    public void run() {
    }
}

Output:

FirstThread is being Executed.
SecondThread is being Executed.

Main Thread

As is common knowledge, each and every Java program has a Main Method that serves as the starting point for the JVM to execute the code. In the same way, each program in the Threading or Multithreading has a Main Thread that is provided by default by the JVM, so whenever a Java program is created, the Main Thread is provided by the JVM for execution.

Thread Priorities in Java

A thread's priority is determined by how many services are assigned these locks in an unusual manner or order. The synchronized keywords plays a role in the causing A deadlock may occur in a multithreaded programming when ed program may to it. Every thread created by the JVM receives priority treatment. The range of priorities is 1 to 10.
1 is given the "Lowest Priority" and 10 is given the "Highest Priority". 5 is called the "Standard Priority". The standard Priority(5) is set as default to the Main Thread. The child thread inherits the priority from its parent thread. The priorities for any thread can be updated when necessary. This is done through the constants provided in the Thread Class. They are:

      1. Thread.NORM_PRIORITTY

      2. Thread.MIN_PRIORITTY

      3. Thread.MAX_PRIORITTY

public class Priority extends Thread
{
    public void run() {
        System.out.println("Priority of the running thread is: " + Thread.currentThread().getPriority());
    }
       public static void main(String[] args) {
        Priority Thread1 = new Priority();
        Priority Thread2 = new Priority();
        Priority Thread3 = new Priority();
        Thread1.setPriority(Thread.MIN_PRIORITY);
        Thread2.setPriority(Thread.MAX_PRIORITY);
        Thread3.setPriority(Thread.NORM_PRIORITY);
        Thread1.start();
        Thread2.start();
        Thread3.start();
    }
    
}

Output:

Priority of the running thread is: 1
Priority of the running thread is: 10
Priority of the running thread is: 5

Thread Synchronization

Multithreading in Java leads to an asynchronous scenario between the programs. This means that when one thread is reading some data and another thread is writing to that data at the same time, may cause issues between the threads and hence in the application.
Synchronization approach is used when a thread wants to read or write data which is in the shared resources. Synchronization methods are provided by java for the implementation of this behaviour. According to this appraoch, when a thread is accessing a data, no other thread can perform operations on the same data. They will have to wait until the thread currently using the data is done with the job and gets out of the synchronization block. This way synchronization helps in multithreading. It can be implemented in the following way.

Synchronized(object)
{  
        //statements to be synchronized
}

Thread Deadlock

The condition in which more than one threads are stuck waiting for each other indefinitely is called DEADLOCK. Deadlock is the result of multiple threads waiting for the same lock but are they are assigned these locks in an unusual manner or order. The synchronized keywords plays a role in the causing of deadlocks. A deadlock condition may occur in a multithreaded programming when this keyword results in executing thread to get blocked while waiting for a lock.
This scenario is illustrated below

public class TestThread {
   public static Object threadLock1 = new Object();
   public static Object threadLock2 = new Object();
   
   public static void main(String args[]) {
      Thread1 D1 = new Thread1();
      Thread2 D2 = new Thread2();
      D1.start();
      D2.start();
   }
   
   private static class Thread1 extends Thread {
      public void run() {
         synchronized (threadLock1) {
            System.out.println("1st Thread: Implementing lock 1...");
            
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("1st Thread: Waiting for lock 2...");
            
            synchronized (threadLock2) {
               System.out.println("1st Thread: Implementing lock 1 & 2...");
            }
         }
      }
   }
   private static class Thread2 extends Thread {
      public void run() {
         synchronized (threadLock2) {
            System.out.println("2nd Thread: Implementing lock 2...");
            
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("2nd Thread: Waiting for lock 1...");
            
            synchronized (threadLock1) {
               System.out.println("2nd Thread : Implementing lock 1 & 2...");
            }
         }
      }
   } 
}

Output:

1st Thread: Implementing lock 1...
2nd Thread: Implementing lock 2...
1st Thread: Waiting for lock 2...
2nd Thread: Waiting for lock 1...

Prevention of Deadlock can be achieved by making sure that when several locks are being handled, the order in which they are acquired must be the same through-out.

Limitations of Threading

Threads are one of the amazing features in Java. But they may also cause several issues when not handled properly. Let us look into the limitations caused by threads.

      Writing the code becomes diffcult and confusing.

      Debugging gets cumbersome.

      Concurrency issues are caused: As all the threads run at the same time, the order in which the code is executed becomes unpredictable.

      The code has to be broken down to minute parts to be tested.

        Porting of the code causes issues.

Conclusion

This concludes our discussion of "Threads in Java." I sincerely hope that you gained some knowledge about the various concepts pertaining to threads in java and are now more interested to dive into the language. You can consult the Java Tutorial for further guidance.

Good luck and happy learning!