Page Content

Tutorials

How are Threads Managed in Java?

Threads Managed in Java

Let’s examine the start() and run() methods, thread states and transitions, thread priorities, and the sleep() and join() methods as we dig into Java’s thread management. Programs can execute many tasks at once thanks to Java’s built-in support for multithreaded programming, which is essential for interactive applications and resource efficiency.

start() and run() Methods

A thread is a unique execution path inside a program in Java. Implementing the Runnable interface or extending the Thread class are the two main methods for creating threads. To maintain and control the thread, both strategies essentially rely on the Thread class.

The new thread’s execution begins with the run() method. The thread’s core business logic is contained in this procedure. It is just as capable as the main thread of declaring variables, using other classes, and calling other methods.

The start() function on a Thread object must be used in order to start a new thread of execution. The run() method is called in a separate thread after the necessary preparation is completed and the thread is registered with the thread scheduler.

It is important to realise that invoking the run() function directly will not start a new thread; rather, it will execute the code of the run() method within the current thread of execution, just like any other method call.

Here’s a simple example demonstrating thread creation using Runnable and the start() method:

// Create a thread by implementing Runnable.
class MyRunnable implements Runnable {
    private String threadName;
    MyRunnable(String name) {
        threadName = name;
        System.out.println("Creating " + threadName);
    }
    public void run() {
        System.out.println("Running " + threadName);
        try {
            for (int i = 3; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", " + i);
                Thread.sleep(50); // Let the thread sleep for a while
            }
        } catch (InterruptedException e) {
            System.out.println("Thread " + threadName + " interrupted.");
        }
        System.out.println("Thread " + threadName + " exiting.");
    }
}
public class ThreadDemo {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");
        
        MyRunnable R1 = new MyRunnable("Child-1");
        new Thread(R1).start(); // Start thread for R1
        MyRunnable R2 = new MyRunnable("Child-2");
        new Thread(R2).start(); // Start thread for R2
        
        System.out.println("Main thread ending.");
    }
}

A possible output for the code above would be:

Main thread starting.
Creating Child-1
Creating Child-2
Running Child-1
Thread: Child-1, 3
Running Child-2
Thread: Child-2, 3
Thread: Child-1, 2
Thread: Child-2, 2
Thread: Child-1, 1
Thread: Child-2, 1
Thread Child-1 exiting.
Thread Child-2 exiting.
Main thread ending.

Thread State Transitions

A thread can exist in a number of different states during its lifetime, including:

Thread State Transitions
Thread State Transitions
  1. New: Following instantiation but before to the call to its start() method, a thread is in the new state. A “born” thread is this one.
  2. Runnable: The thread is made runnable by using start(). This indicates that its work is either in progress or will be ready to run as soon as it has access to the CPU.
  3. Waiting: When a thread pauses its execution until another thread completes a certain task and gives it the all-clear to proceed, it enters the waiting state. Examples include using join() on a different thread or invoking wait() without a timeout.
  4. Timed Waiting: Like the waiting state, except when a predetermined amount of time has passed or if it is informed, the thread will revert to the runnable state, whichever occurs first. This state can be caused by methods such as join(long milliseconds), sleep(), or wait(long milliseconds).
  5. Terminated (Dead): When a thread’s run() method finishes its job, exits normally, or is otherwise stopped, it enters the Terminated (Dead) state. It is impossible to restart a thread once it has ended.

Transitions between these states are managed by the Java Virtual Machine (JVM) and explicit method calls. For instance, a New thread becomes Runnable after start(). A Runnable thread might enter Timed Waiting via sleep() or Waiting via wait(). A Waiting or Timed Waiting thread can become Runnable if notified by notify()/notifyAll() or if its waiting period expires. Finally, from any active state, a thread will become Terminated once its work is done.

Thread Priorities

Java gives each thread a priority, which is an integer value that represents how important it is in relation to the others. Thread priorities range from MIN_PRIORITY (1) to MAX_PRIORITY (10), with NORM_PRIORITY (5) being the default.

  • setPriority(int level): This method allows you to change a thread’s priority. The level must be within the valid range.
  • getPriority(): This method returns the current priority setting of a thread.

Although a higher-priority thread often gets more CPU time than a lower-priority one, priorities are heavily reliant on how the operating system implements scheduling and do not ensure execution order. Because Java employs preemptive multitasking, a higher-priority thread may execute concurrently with a lower-priority thread.

Also, threads can use the yield() method to willingly give up control of the CPU. This enables scheduling of other threads with the same priority. Once a thread has finished a portion of its job, it is courteously allowed to share CPU time.

sleep() Method

The presently running thread pauses its operation for a predetermined number of milliseconds when the Thread.sleep() method is used. Since this procedure is static, the active thread is always impacted.

  1. The basic form is static void sleep(long milliseconds) throws InterruptedException.
  2. Calls to sleep() are typically wrapped in a try-catch block to handle the InterruptedException that may be raised if the sleeping thread is interrupted by another thread.
  3. No locks that the thread may be holding are released by the sleep() method, in contrast to wait(). To comprehend thread synchronisation, this distinction is essential.

Here’s an example of sleep():

// Example of Thread.sleep()
public class SleepDemo {
    public static void main(String[] args) {
        System.out.println("Main thread starting.");
        try {
            for (int n = 5; n > 0; n--) {
                System.out.println("Main Thread: " + n);
                Thread.sleep(1000); // Pause for 1 second
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

Output:

Main thread starting.
Main Thread: 5
Main Thread: 4
Main Thread: 3
Main Thread: 2
Main Thread: 1
Main thread exiting.

join() Method (Waiting for a thread to finish)

By using the join() method, you may make sure that a thread finishes running before the thread that requested join() does. Until the target thread ends or the designated timeout expires, the calling thread will halt (in the Waiting or Timed Waiting state) when it calls join() on another thread.

  • The basic form is final void join() throws InterruptedException.
  • Overloaded versions, such as join(long milliseconds), let you choose a maximum waiting duration.
  • Because it eliminates unnecessary timing assumptions, using join() to wait for a thread to finish is more robust and dependable than just using Thread.sleep() in the caller thread.

Here’s an example demonstrating join():

// Example using join()
class ChildThread implements Runnable {
    private Thread t;
    private String threadName;
    ChildThread(String name) {
        threadName = name;
        System.out.println("Creating " + threadName);
        t = new Thread(this, threadName);
        t.start(); // Start the thread
    }
    public void run() {
        System.out.println("Running " + threadName);
        try {
            for (int i = 3; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", " + i);
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            System.out.println("Thread " + threadName + " interrupted.");
        }
        System.out.println("Thread " + threadName + " exiting.");
    }
}
public class JoinDemo {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");
        
        ChildThread child1 = new ChildThread("Child-1");
        ChildThread child2 = new ChildThread("Child-2");
        try {
            System.out.println("Waiting for child threads to finish.");
            child1.t.join(); // Main thread waits for child1 to finish
            child2.t.join(); // Main thread waits for child2 to finish
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

Output:

Main thread starting.
Creating Child-1
Creating Child-2
Running Child-1
Running Child-2
Waiting for child threads to finish.
Thread: Child-1, 3
Thread: Child-2, 3
Thread: Child-1, 2
Thread: Child-2, 2
Thread: Child-1, 1
Thread: Child-2, 1
Thread Child-1 exiting.
Thread Child-2 exiting.
Main thread exiting.

The “Main thread exiting.” message only shows up after the “Child-1 exiting.” and “Child-2 exiting.” messages in this output, indicating that join() made the main thread wait for the child threads to finish.

In order to create Java concurrent applications that are reliable, effective, and responsive, it is essential to grasp these ideas.

Index