Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency: Java Multithreading A Comprehensive Guide Part 2
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
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:
Thread t = new Thread();
: This constructor creates a new thread objectt
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 callingt.start()
will cause the thread to execute therun()
method of theThread
class, which has an empty implementation.Thread t = new Thread(Runnable r);
: This constructor creates a new thread objectt
and associates it with the specifiedRunnable
objectr
. Whent.start()
is called, therun()
method of theRunnable
objectr
will be executed by the new thread.Thread t = new Thread(String name);
: This constructor creates a new thread objectt
with the specified name. The thread is considered to be in a “new” state, and callingt.start()
will cause the thread to execute therun()
method of theThread
class, which has an empty implementation.Thread t = new Thread(Runnable r, String name);
: This constructor creates a new thread objectt
associated with the specifiedRunnable
objectr
and with the specified name.Thread t = new Thread(ThreadGroup g, String name);
: This constructor creates a new thread objectt
within the specifiedThreadGroup
g
and with the specified name.Thread t = new Thread(ThreadGroup g, Runnable r);
: This constructor creates a new thread objectt
within the specifiedThreadGroup
g
and associates it with the specifiedRunnable
objectr
.Thread t = new Thread(ThreadGroup g, Runnable r, String name);
: This constructor creates a new thread objectt
within the specifiedThreadGroup
g
, associates it with the specifiedRunnable
objectr
, and gives it the specified name.Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);
: This constructor creates a new thread objectt
within the specifiedThreadGroup
g
, associates it with the specifiedRunnable
objectr
, 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:
p
ublic final String getName()
: This method returns the name of the thread as aString
.public final void setName(String name)
: This method sets the name of the thread to the specifiedname
.
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 threadt
, 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()
.
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:
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 themain()
method is executed by the main thread. - In the
run()
method ofMyThread
,Thread.currentThread().getName()
returns the name of the current thread, which is “Thread-0”. This is because therun()
method is executed by the thread represented by theMyThread
objectt
, 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
–> 1Thread.NORM_PRIORITY
–> 5Thread.MAX_PRIORITY
–> 10
Identifying valid thread priorities:
- 0: Invalid
- 1: Valid
- 10: Valid
Thread.LOW_PRIORITY
: Invalid (not a standard constant in theThread
class)Thread.HIGH_PRIORITY
: Invalid (not a standard constant in theThread
class)Thread.MIN_PRIORITY
: Valid (constant defined in theThread
class)Thread.NORM_PRIORITY
: Valid (constant defined in theThread
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)
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.
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:
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:
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:
yield()
join()
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()
:
public static native void yield();
Impact of yield() method on thread life cycle:
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:
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:
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:
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:
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:
main thread
main thread
.
.
10 times
.
child thread
.
.
10 times
.
Explanation
- In this example, the child thread (
MyThread
) calls thejoin()
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 themt
static variable of theMyThread
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.
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.
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:
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.
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:
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.
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:
End of main thread
I am lazy thread
I got interrupted
- In this example, the
main
thread interrupts theMyThread
thread by calling theinterrupt()
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.
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:
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 aMyThread
thread and immediately calls theinterrupt()
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 usingThread.sleep(10000)
, and the interrupt call immediately interrupts it, causing it to throw anInterruptedException
. - 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.