Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency: Java Multithreading A Comprehensive Guide Part 2

Table of Contents

Threads are the lifeblood of concurrent programming in Java, enabling multiple tasks to execute seemingly simultaneously, enhancing the performance of applications by utilizing the available processing power efficiently. Understanding thread constructors and methods is crucial for effectively creating, managing, and synchronizing threads in your applications. In this blog post, we’ll delve into the various constructors and methods provided by the Thread class in Java. Specially three fundamental methods: yield(), join(), and sleep(). Each of these methods serves a specific purpose in controlling thread execution, offering developers flexibility and control over concurrent processes.

Thread Constructors

Java
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);

let’s break down each constructor of the Thread class:

  1. Thread t = new Thread();: This constructor creates a new thread object t but does not specify the task (runnable) for the thread to execute. In this case, the thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  2. Thread t = new Thread(Runnable r);: This constructor creates a new thread object t and associates it with the specified Runnable object r. When t.start() is called, the run() method of the Runnable object r will be executed by the new thread.
  3. Thread t = new Thread(String name);: This constructor creates a new thread object t with the specified name. The thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  4. Thread t = new Thread(Runnable r, String name);: This constructor creates a new thread object t associated with the specified Runnable object r and with the specified name.
  5. Thread t = new Thread(ThreadGroup g, String name);: This constructor creates a new thread object t within the specified ThreadGroup g and with the specified name.
  6. Thread t = new Thread(ThreadGroup g, Runnable r);: This constructor creates a new thread object t within the specified ThreadGroup g and associates it with the specified Runnable object r.
  7. Thread t = new Thread(ThreadGroup g, Runnable r, String name);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, and gives it the specified name.
  8. Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, gives it the specified name, and specifies the size of the thread’s stack.

Getting and Setting the Name of a Thread

Every thread in Java has a name, which may be a default name generated by the JVM or a customized name provided by the programmer. We can get and set the name of a thread using the following two methods of the Thread class:

  • public final String getName(): This method returns the name of the thread as a String.
  • public final void setName(String name): This method sets the name of the thread to the specified name.
Java
class MyThread extends Thread {
}

class Test {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());  // main
    MyThread t = new MyThread();
    System.out.println(t.getName()); // Thread-0
    Thread.currentThread().setName("softAai Apps");
    System.out.println(Thread.currentThread().getName()); // softAai Apps
  }
}

Here,

  • Thread.currentThread().getName() returns the name of the current thread, which is “main” by default.
  • t.getName() returns the name of the thread t, which is “Thread-0” by default since it’s created without a specific name.
  • Thread.currentThread().setName("softAai Apps") sets the name of the current thread to “softAai Apps”.
  • Thread.currentThread().getName() returns the updated name of the current thread, which is now “softAai Apps”.

These methods provide a way to manage and identify threads in Java programs.

Getting the Current Executing Thread Object

We can get the current executing thread object by using Thread.currentThread().

Java
class MyThread extends Thread {
  public void run() {
    System.out.println("run method executed by Thread: " + Thread.currentThread().getName());
  }
}

class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start()
    System.out.println("main method executed by Thread: " + Thread.currentThread().getName());
  }
}

Output:

Java
main method executed by Thread: main
run method executed by Thread: Thread-0
  • In the main() method, Thread.currentThread().getName() returns the name of the current thread, which is “main”. This is because the main() method is executed by the main thread.
  • In the run() method of MyThread, Thread.currentThread().getName() returns the name of the current thread, which is “Thread-0”. This is because the run() method is executed by the thread represented by the MyThread object t, which has the default name “Thread-0”.

Using Thread.currentThread() allows you to obtain information about the thread currently executing a particular block of code, which can be useful for debugging and logging purposes.

Thread Priority

Every thread in Java has some priority, which may be a default thread priority generated by the JVM or a customized priority provided by the programmer. The valid range of thread priorities is from 1 to 10, where 1 is the minimum thread priority and 10 is the maximum thread priority. The Thread class defines the following constants to represent some standard priorities:

  • Thread.MIN_PRIORITY –> 1
  • Thread.NORM_PRIORITY –> 5
  • Thread.MAX_PRIORITY –> 10

Identifying valid thread priorities:

  • 0: Invalid
  • 1: Valid
  • 10: Valid
  • Thread.LOW_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.HIGH_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.MIN_PRIORITY: Valid (constant defined in the Thread class)
  • Thread.NORM_PRIORITY: Valid (constant defined in the Thread class)

If two threads have the same priority, then we can’t expect the exact execution order; it depends on the thread scheduler.

The Thread class defines the following methods to get and set the priority of a thread:

  • public final int getPriority()
  • public final void setPriority(int p) (allowed values range: 1 to 10; otherwise, IllegalArgumentException is thrown)
Java
t.setPriority(7);  // valid
t.setPriority(17); // IllegalArgumentException

In this example, setting the priority to 7 is valid, but setting it to 17 would result in an IllegalArgumentException because it’s outside the valid range.

Default Thread Priority

The default thread priority for the main thread is 5, but for all other threads, the default thread priority will be inherited from the parent thread. This means that whatever priority the parent thread has, the same priority will be inherited by the child thread.

Java
class MyThread extends Thread {
}

class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getPriority());  // 5 (default priority for main thread)
        
        // Thread.currentThread().setPriority(15);  // Throws IllegalArgumentException
        // Thread.currentThread().setPriority(7);   // --> line 1 
        
        // line 1 allowed and sets 7 as priority, but it's redundant as the default priority is 5
        
        MyThread t = new MyThread();
        System.out.println(t.getPriority());  //initial output 7 if line 1 is not commented,  otherwise 5 (as inherited from the parent thread, which is the main thread)
    }
}

If we comment line 1, then the child thread priority will become 5, inherited from the parent thread.

MyThread t = new MyThread();
Here, the parent class is Thread class, and its parent thread is the Main Thread.
Parent thread and parent class are different entities.

Thread Priority Example

Let’s discuss the scenario of setting thread priorities and its implications:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("child thread");
    } 
  }
}

class ThreadPriorityDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    //t.setPriority(10);   --------->  line 1
    t.start();
   
    for(int i=0; i<10; i++) { 
      System.out.println("main thread");
    }
  }
}

If we comment out line 1, then both the child thread and the main thread will have the same priority, which is 5. Hence, we can’t expect the execution order and the exact output, as we don’t know which thread will get the first chance.

If we don’t comment out line 1, then the main thread has priority 5 and the child thread has priority 10. In this case, the child thread will get the chance first, followed by the main thread. The output will be:

Java
child thread
child thread
(child thread repeated 10 times)
main thread
(main thread repeated 10 times)

Note: Some platforms may not provide proper support for thread priorities. In such cases, even if we set high priority, we may not get the exact output. This is a system problem, and there’s nothing we can do about it.

Ways to Temporarily Prevent Thread Execution

We can prevent a thread’s execution temporarily by using the following methods:

  1. yield()
  2. join()
  3. sleep()

yield() Method

yield() causes the current executing thread to pause and gives a chance for waiting threads of the same priority. If there are no waiting threads or all waiting threads have a lower priority, then the same thread can continue its execution. If multiple threads are waiting with the same priority, we can’t predict which waiting thread will get the chance; it depends on the thread scheduler. When the thread that yielded will get a chance again also depends on the thread scheduler, and we can’t predict it precisely.

Complete prototype of yield():

Java
public static native void yield();

Impact of yield() method on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Child Thread");
      Thread.yield();  // Commented line 1
    }   
  }
}

class ThreadYieldDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    for(int i=0; i<10; i++) {
      System.out.println("main Thread");
    }
  }
}

In the above program, if we comment out line 1, then both threads will be executed simultaneously, and we can’t predict which thread will complete first. If we don’t comment out line 1, then the child thread will always call yield(), causing the main thread to get more chances to execute, increasing the likelihood of the main thread completing first.

Note: Some platforms may not provide proper support for yield().

join() Method

This method waits for a thread to complete its execution. When a thread calls join() on another thread, it will wait until the specified thread completes its execution before continuing its own execution. There are several overloaded versions of the join() method that allow you to specify a timeout duration for the waiting period.

  • Prototype of join() method:
Java
public final void join() throws InterruptedException
public final void join(long ms) throws InterruptedException
public final void join(long ms, int ns) throws InterruptedException

Note: Every join() method throws InterruptedException, which is a checked exception. Hence, we must handle it using either try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Usage of join() Method

For example, if a thread t1 wants to wait until another thread t2 completes, then t1 has to call t2.join(). When t1 executes t2.join(), it immediately enters a waiting state until t2 completes. Once t2 completes, t1 can continue its execution.

Example Scenario:

Venue Fixing activity (t1)
Wedding card printing (t2) —- t1.join() —-
Wedding card distribution (t3) —– t2.join() —-

The wedding card printing thread has to wait until the venue fixing thread, hence t2 needs to call t1.join(). Similarly, the wedding card distribution thread has to wait until the wedding card printing thread, hence t3 needs to call t2.join().

Impact of join() on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Seetha Thread");
      try {
        Thread.sleep(2000);
      } catch(InterruptedException e) {
        // Handle the exception
      }
    }
  }
}
 
class ThreadJoinDemo {
  public static void main(String[] args) throws InterruptedException {
    MyThread t = new MyThread();
    t.start();
  
    t.join();     // Commented line 1   (Main thread waits for the child thread to complete)
   
    for(int i=0; i<10; i++) {
      System.out.println("Rama Thread");
    }
  }
}

In the above program, if we comment out line 1, then both the main and child threads will execute simultaneously, and we can’t predict the exact output. If we don’t comment out line 1, then the main thread calls join() on the child thread object, causing the main thread to wait until the child thread completes. In this case, the output will be:

Java
Seetha Thread
Seetha Thread
.. (repeated 10 times)

Rama Thread
Rama Thread 
.. (repeated 10 times)

Waiting of Child Thread Until Completing Main Thread

Let’s discuss the scenario where the child thread waits until the completion of the main thread using the join() method:

Java
class MyThread extends Thread {
    static Thread mt; //this line holds the main thread object in the main method

    public void run() {
        try {
            mt.join(); // Child thread waits until the main thread completes
        } catch (InterruptedException e) {
            // Handle the exception
        }

        for (int i = 0; i < 10; i++) {
            System.out.println("child thread");
        }
    }
}

class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mt = Thread.currentThread();
        MyThread t = new MyThread();
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main thread");
            Thread.sleep(2000);
        }
    }
}

In the above example, the child thread calls join method on the main thread object, hence the child thread has to wait until the main thread completes. In this case, the output is:

Java
main thread
main thread 
.
.
10 times
.
child thread 
.
.
10 times 
.

Explanation

  • In this example, the child thread (MyThread) calls the join() method on the main thread object (mt). This causes the child thread to wait until the main thread completes its execution.
  • The main thread is identified by Thread.currentThread(), and its reference is stored in the mt static variable of the MyThread class.
  • After starting the child thread, the main thread continues its execution. It prints “main thread” ten times with a 2-second delay between each print statement.
  • Meanwhile, the child thread is in the waiting state due to the join() method. Once the main thread completes its execution, the child thread resumes and prints “child thread” ten times.
  • This ensures that the child thread waits for the main thread to finish before continuing its own execution.

Deadlock Situation in case of join() method

Let’s discuss two scenarios involving the join() method that can lead to program deadlock:

Case 1: Deadlock between Main Thread and Child Thread:

If the main thread calls the join method on the child thread object, and the child thread also calls join() on the main thread object, then both threads will wait indefinitely, resulting in a deadlock. This situation resembles a deadlock scenario where neither thread can proceed.

Java
class MyThread extends Thread {
    static Thread mainThread;

    public void run() {
        try {
            mainThread.join(); // Child thread waits for main thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Child thread finished.");
    }
}

class ThreadDeadlockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mainThread = Thread.currentThread(); // Save reference to main thread
        MyThread t = new MyThread();
        t.start();

        try {
            t.join(); // Main thread waits for child thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Main thread finished.");
    }
}

Case 2: Deadlock within the Same Thread:

If a thread calls the join method on itself, then the program will be stuck, similar to a deadlock situation. In this case, the thread has to wait indefinitely for itself to complete, which cannot happen.

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().join(); // Main thread waits for itself
    }
}

In the above example, the main thread is calling join() on itself, causing the program to get stuck indefinitely.

Note: In both cases, the program will be stuck indefinitely, unable to proceed further, due to deadlock situations. These scenarios illustrate the importance of using join() method with caution to avoid deadlocks.

sleep() Method

If a thread doesn’t need to perform any operation for a specific amount of time, then it should use the sleep method. This method causes the currently executing thread to sleep (temporarily suspend execution) for the specified duration.

Prototypes:

Java
public static native void sleep(long ms) throws InterruptedException
public static void sleep(long ms, int ns) throws InterruptedException

Note:
Every sleep() method throws InterruptedException, which is a checked exception. Therefore, whenever we use the sleep method, we must handle InterruptedException either by using try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Impact of sleep method on Thread life cycle

When a thread calls the sleep() method, it enters the TIMED_WAITING state for the specified duration. During this time, the thread does not consume CPU resources and is not eligible for execution. After the specified duration elapses, the thread transitions back to the RUNNABLE state and continues its execution.

Java
class SlideRotator {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Slide-" + i);
            Thread.sleep(5000); // Sleep for 5 seconds
        }
    }
}
  • In this example, the main method prints the slides from 1 to 10 and then sleeps for 5 seconds (Thread.sleep(5000)) before printing the next slide.
  • After printing each slide, the thread sleeps for 5 seconds before proceeding to print the next slide.
  • During the sleep period, the thread transitions to the TIMED_WAITING state, where it remains until the specified duration (5 seconds) elapses.
  • Once the sleep duration is over, the thread transitions back to the RUNNABLE state and continues its execution by printing the next slide.

So, in the above example, the thread pauses for 5000 milliseconds (5 seconds) after printing each slide. This simulates a slide rotation process where each slide is displayed for 5 seconds before moving to the next one.

interrupt() Method

A thread can interrupt a sleeping or waiting thread by using the interrupt() method of the Thread class. This method interrupts the execution of the target thread, causing it to throw an InterruptedException if the target thread is in a sleeping or waiting state.

Prototype:

Java
public void interrupt();

When a thread calls interrupt() on another thread, it sets the interrupt flag for that thread. If the target thread is in a blocking method such as sleep() or wait(), it will throw an InterruptedException immediately, or the next time it enters a blocking operation.

Java
class MyThread extends Thread {
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("I am lazy thread");
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            System.out.println("I got interrupted");
        }
    }
}

class ThreadInterruptedDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        t.interrupt(); // Commented line 1 (Interrupt the child thread)
        System.out.println("End of main thread");
    }
}

If we comment out line 1, then the main thread won’t interrupt the child thread. In this case, the child thread will execute the for loop 10 times.

If we don’t comment out line 1, then the main thread interrupts the child thread. In this case, the output is:

Java
End of main thread 
I am lazy thread
I got interrupted
  • In this example, the main thread interrupts the MyThread thread by calling the interrupt() method on it.
  • If line 1 is commented out, the main thread won’t interrupt the child thread. In this case, the child thread executes the for loop 10 times without interruption.
  • If line 1 is not commented out, the main thread interrupts the child thread. As a result, the child thread throws an InterruptedException, and the message “I got interrupted” is printed.
  • Regardless of whether the child thread is interrupted or not, the message “End of main thread” is always printed at the end of the main thread’s execution.

Note: Whenever we call the interrupt method, if a target thread is not in a sleeping state or waiting state, then there is no immediate impact of the interrupt call. The interrupt call will be queued until the target thread enters a sleeping or waiting state. If the target thread is in a sleeping or waiting state, then the interrupt call will immediately interrupt the target thread.

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<=10000; i++) {
      System.out.println("I am lazy thread-"+i);
    }
    System.out.println("I am entering into sleeping state");
    try {
      Thread.sleep(10000);
    } catch(InterruptedException e) {
      System.out.println("I got interrupted");
    }
  }
}

class ThreadSleepDemo1 {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    t.interrupt();  // Interrupt call is made immediately
    System.out.println("End of main thread");
  }
}

In the above example, the target thread t is interrupted immediately after the start method is called. However, since the target thread is not in a sleeping or waiting state yet, the interrupt call will not have any immediate effect. The interrupt call will be queued until the target thread enters the sleeping state. Therefore, in this case, the output will be:

Java
End of main thread
I am lazy thread-0
I am lazy thread-1
...
I am lazy thread-9999
I am entering into sleeping state
I got interrupted
  • In this example, the main thread starts a MyThread thread and immediately calls the interrupt() method on it.
  • However, since the MyThread thread is executing a loop and not in a sleeping or waiting state, the interrupt call has no immediate effect.
  • The interrupt call will be queued until the MyThread thread enters a sleeping or waiting state.
  • In this case, the MyThread thread eventually enters a sleeping state using Thread.sleep(10000), and the interrupt call immediately interrupts it, causing it to throw an InterruptedException.
  • If the MyThread thread never enters a sleeping or waiting state, the interrupt call would have no effect throughout its execution.

Quick Revision: yield(), join(), and sleep() methods

Let’s discuss yield(), join(), and sleep() methods in Java, along with their characteristics:

yield()

Purpose: If a thread wants to pause its execution to give a chance for the remaining threads of the same priority, then we should use yield().

  • Is it Overloaded? No,
  • Is it final? No
  • Does it throw InterruptedException? No
  • Is it native? Yes
  • Is it static? Yes

join()

Purpose: If a thread wants to wait until completing some other thread, then we should use join().

  • Is it Overloaded? Yes
  • Is it final? Yes
  • Does it throw InterruptedException? Yes
  • Is it native? No
  • Is it static? No

sleep()

Purpose: If a thread doesn’t want to perform any operation for a particular amount of time, then we should use the sleep method.

  • Is it Overloaded? Yes
  • Is it final? No
  • Does it throw InterruptedException? Yes
  • Is it native? sleep(long ms) is native, sleep(long ms, int ms) is non-native
  • Is it static? Yes

Part 3: Mastering Synchronization in Java Threads

Conclusion

Thread management is a crucial aspect of developing robust and efficient multithreaded Java applications. The yield(), join(), and sleep() methods provide valuable tools for controlling thread execution, synchronization, and timing.

By leveraging these methods effectively, developers can orchestrate concurrent activities, coordinate thread interactions, and manage resource contention, thereby enhancing the performance and reliability of their applications in a concurrent environment.

Understanding the nuances of these thread-related methods empowers developers to design and implement multithreaded applications that exhibit optimal concurrency behavior, scalability, and responsiveness, contributing to the development of high-performance Java software systems.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!