Everything about Threads in Java

Jalitha Dewapura
Java For Beginners
Published in
10 min readMay 13, 2021

Some tasks can perform parallelly. It applies to programs as well. It doesn’t mean if there is a process with five tasks and it takes 10mins to complete, the process can complete within 2mins by five parallel tasks. Because there some dependency tasks. Somehow it can be completed in less than 10mins. When considering an enterprise-level application, there are hundreds of tasks and it will be more efficient if the programmer divides them into multiple threads properly. The following example will be helpful to understand the dependency tasks.

Three threads are the maximum number of threads that can be used to optimize the Process. Because tasks C and D are depending on other tasks.

Thread Life Cycle

Thread States

The life cycle of the thread in java is controlled by JVM. There are five states a thread can stay from birth to death. Whenever a thread goes to the death state that cannot be revert.

  • New — The thread is in a new state if you create an instance of Thread class but before the invocation of the start() method.
  • Runnable — The thread is in the runnable state after the invocation of the start() method, but the thread scheduler has not selected it to be the running thread.
  • Running — The thread is in the running state if the thread scheduler has selected it.
  • Waiting/Sleeping/Blocking — This is the state when the thread is still alive but is currently not eligible to run.
  • Terminated — A thread is in the terminated state when it's run() method exits.

Note: If you have any doubt about these states, please continue the article. I will explain it deeply by examples.

Types of Threads

There are two types of threads in Java

  • Daemon thread — provides services to user threads for background supporting tasks. It has no role in life than to serve user threads. And it a low-priority thread. ex:- garbage collector, finalizer, etc.
  • User thread (non-daemon thread) — designed to do some specific or complex task and it’s a high priority thread. When the task is completed, the thread goes to the terminate state.

When creating a thread, it’s created as a user thread. But the Thread class has a method call setDaemon() that can convert a user thread into a daemon thread. As we know the JVM executes until the last non-daemon thread exits. Therefore, it does not guarantee the execution of the thread which you make as a daemon thread.

Main Thread

As we know the main method is the place that starts the execution when the program starts. When considering the perspective of the threads, it is called the main thread. If there is not any child thread, this is where you can create a new thread.

Multithreading

As I explain, every program has at least the main thread. Child threads can be created inside any user thread. There are two ways to create a child thread in Java.

  • Creating a child thread using Thread class
  • Create a child thread using Runnable interface

Create a child thread using Thread class

First, we need to create a class that extends from the Thread class and overrides the run() method. This is where you must implement what you want to execute in this thread. In this example, “Child Thread” will be printed when the thread is executed.

public class Printer extends Thread { 
@Override
public void run(){
System.out.println("Child Thread");
}
}

In order to execute this thread, we need to create an instance from this class in the main thread (any user thread which executes when the program is running). Then we need to call the start() method using that instance as bellow.

public class Application {
public static void main(String[] args){
Printer printer = new Printer(); //new state
//upto this point, there is only main thread.
printer.start();//child thread ready the excution. runnable state
System.out.println("Main Thread");
}
}

Note: When you are dealing with multithreading, there is no guarantee which thread will execute first. It totally depends on thread scheduler.

Note: If there is a runtime exception in main thread after calling the start mthod in the child thread, the child thread works fine. But if there is a runtime exception before start the child thread, it is not going to work.

Now you have a basic idea about thread and how to execute it. Let me explain the disadvantages of using this method.

Assume this Printer class has to be inherited from the Machine class. At the moment the Printer class extends from the Thread class, it will break the parent-child relationship.

To overcome this disadvantage, we can use the Runnable interface.

Create a child thread using Runnable interface

Let’s take the previous example with the Runnable interface.

public class Printer extends Machine implements Runnable { 
@Override
public void run(){
System.out.println("Child Thread");
}
}

Since we can have multiple implementations in Java, this is a good way to overcome the previous disadvantage. In this scenario, we don’t have any method called start() in the Runnable interface. Therefore, we need an instance from the Thread class and pass the runnable instance as an argument.

Note: Actually the Runnable instance is a single abstract method interface. It consists only of the run() method.

public class Application {
public static void main(String[] args){
Printer printer = new Printer();
Thread printerThread = new Thread(printer);

printerThread.start();
}
}

There are eight different constructors in the Thread class.

Multiple Constructors of Thread class (https://www.tutorialspoint.com/)

At the moment, you know two different ways to create a child thread. So, let’s see few common questions which you might be wondering.

What happened if the run() method is not overridden?

If you use Thread class to create the child thread, it must not be overridden the run() method. But if you do not override it, nothing will happen from that thread.

If you use Runnable interface, we must override it. Because it’s an abstract method.

What happened if the start() method is overridden?

Nothing happens. It just executes a normal method call. Just like creating an instance of a particular class and call the start() method.

What happened if we directly invoke the run() method without calling the start() method?

When we invoke the run method, it means we are not giving an opportunity to create a new thread. As a result, it just executes a normal method call.

Actually, this start() method involves many things such as:

  • check whether this thread is already exist
  • check whether this thread is ready to run
  • is it need to register and then it will be added to the thread pool

Can we overload the run() method?

Yes, we can. But the start() method always involves run() method with no argument.

Multithreading with a daemon thread

Let’s consider the example below.

public class Printer extends Thread { 
@Override
public void run(){
for(int i=0; i<1000; i++){
System.out.println(currentThread().getName()+" "+i);
}
}
}

Use setDaemon() method to set this printer thread as a daemon thread.

public class Application {
public static void main(String[] args){
Printer printer = new Printer();
printer.setDaemon(true);
printer.start();
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println("=====================");
}
}

In this case, the application terminates the execution when the main thread exits. It means the printerThread might not be able to print up to “Thread-0 999”

Note: The last line of the main method (the line with ‘=’ marks) may not be the last printing statement. Because the printing mechanism on console is working separately from the JVM. Anyway, the JVM will stop the execution when the main method exits.

Thread Priority

The order of executions cannot be predicted in Java Threads. However, Java introduces a way to assign thread priorities for the threads one over another. This priority has a range of values from 1 to 10. 1 is the lowest and 10 is the highest priority. If someone sets the priority beyond the range, it will throw an exception called ‘illegalArgumentException’.

Let’s take examples and see how to set the priority.

public class Printer extends Thread { 
@Override
public void run(){
for(int i=0; i<1000; i++){
System.out.println(currentThread().getName()+" "+i);
}
}
}

This printer class will not be changed for the next few examples.

Example 1

public class Application {
public static void main(String[] args){
Printer printer = new Printer();
System.out.println("Printer before: "+ printer.getPriority());
printer.setPriority(10);
System.out.println("Main: "+ printer.getPriority());
System.out.println("Printer after: "+ printer.getPriority());
printer.start();
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println("=====================");
}
}

If the priority is set as maximum, it is not guaranteed that this thread will finish before the completion of others.

Output 1:

Printer before: 5
Main: 5
Printer before: 10
Thread-0 1
Thread-0 2
. . .

Example 2

public class Application {
public static void main(String[] args){
System.out.println("Main Before: "+ ThreadgetPriority.getPriority());
Thread.currentThread().setPriority(7); //Main thread
Printer printer = new Printer();
System.out.println("Main: "+ printer.getPriority());
System.out.println("Printer: "+ printer.getPriority());
printer.start();
for(int i=0; i<100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println("=====================");
}
}

Output 2:

Main before: 5
Main: 7
Printer before: 7
Main 1
Thread-0 1
. . .

The default priority value of the main thread is 5. Because the main thread is created by the system and they set it as 5. The default value of child threads will be the parent thread priority value at the point it created.

Thread Pool and Thread Scheduler

The thread pool is the place every thread will wait until it’s selected to execute.

The thread scheduler is the part of the JVM that decides which thread should be executed. There is no guarantee that which thread will be selected to execute by the thread scheduler.

Preemptive scheduling: The highest priority task executes until it goes to the waiting state or dead state or another higher priority task comes into the scheduler it will execute.

Time Slicing: The given task will execute for a predefined time period and then reenters the pool of ready tasks. Then the scheduler will decide which task will execute next.

Thread Join

In Java, the Thread class provides the join() method which allows one thread to wait until another thread completes its execution. Let’s consider there are two threads called t1 and t2. if the t1 thread calls join() method, t1 has to wait until t2 complete.

Inside the t1's run() method,

t2.join();

The join() method has three overloaded methods.

  • join() — this no-argument method will keep waiting until the other thread completes its task.
  • join(long milliseconds) — this long-argument method will keep waiting until either other thread completes its task or exceeds the specified time limit(in milliseconds).
  • join(long milliseconds, int nanoseconds) — this long-argument method will keep waiting until either other thread completes its task or exceeds the specified time limit(in milliseconds and nanoseconds).

Thread Wait and Block states

A thread goes to the waiting state when it calls join() or wait() method. As I explained above, once a thread calls the join() method, it will wait until either other thread completes its task or exceeds the given certain time limit. When the other thread completed its task, it calls either notify() or notifyAll() method. Then the thread goes to the runnable state as usual.

A thread moves to the blocked state when it wants to access an object that is being used (locked) by another thread. Once that resource is available (unlocked) for the thread, it is no longer blocked and moves to the runnable state.

Thread Sleep

Once the sleep() method is called, it goes to the sleep state until the exceeds a given certain time. The sleep() method has two overloaded methods.

  • sleep(long milliseconds) — given time in milliseconds and this is a native method().
  • sleep(long milliseconds, int nanoseconds) — the time can be more specified with milliseconds and nanoseconds

Whenever you use this sleep() method, you have to handle the ‘InterruptedExeption’. Because the thread sleep can be interrupted by the interrupt() method.

try {
Thread.sleep(250);
} catch (InterruptedException e) {
System.out.println("wake up by calling the interrupt method");
}

Thread Interrupt

If any thread is in the sleeping or waiting state, it can be interrupted by some other thread.

  • public void interrupt() — if the thread is sleeping, it will be interrupted or otherwise, it set the interrupted flag to true.
  • public static boolean interrupted() — if the thread is not interpreted, it sets the interrupted flag to true and returns true. Otherwise, it returns false.
  • public boolean isInterrupted() — returns the interrupted flag either true or false.

If the thread is not in the sleeping or waiting state, calling the interrupt() method sets the interrupted flag to true. Once it goes to the sleeping state, it will be interrupted immediately.

Thread Yield

The yield() method is used to stop the current execution of the thread(temporarily) and give their opportunity to execute some other thread. The yield() method does not allow to decide which thread should run next. It will decide by the thread scheduler. When a thread calls the yield method, it is moved to the thread pool immediately. There is no guarantee that this thread will execute next time. For example, there are three threads calls t1, t2, and t3. If the t1 thread calls the yield() method, t2 or t3 will get the opportunity. Let’s assume the t2 gets the opportunity and t2 completed its job. Then there is no guarantee that the t1 will get the opportunity next. it’s up to the thread scheduler.

public class t1 extends Thread { 
@Override
public void run(){
. . .
Thread.yield(); // at this point, t1 notifies the thread scheduler to give the opportunity.
. . .
}
}

Synchronization

Once you dealing with a multi-threaded program, there are some situations where multiple threads try to access the same resources and finally produce erroneous and unforeseen results. Thus, Java has introduced a synchronization method that only one thread can access the resource at a given point in time.

Synchronized keyword — used to indicate that a method can be accessed by only one thread at a time

Let’s take an example.

class Test {
int count = 0;
public void increment(){
count++;
}
}

Assume there are two threads called t1 and t2. Those threads use this increment() method simultaneously. Final results cannot be predicted. Because this method is not thread-safe. Therefore, we need to insert the synchronized keyword.

class Test {
int count = 0;
public synchronized void increment(){
count++;
}
}

Now, this method is thread-safe and will be used only one thread at a time.

I hope you learn something about Java threads. If I missed any points, let me know your suggestion in the comment section. And these videos uploaded by Krish will be really helpful to understand Java threads.

--

--

Jalitha Dewapura
Java For Beginners

BSc. (Hons) in Engineering, Software Engineer at Virtusa