reCAPTCHA WAF Session Token
Information Technology

Thread behavior in the JVM

Threading refers to the practice of executing programming processes concurrently to improve application performance. While it’s not that common to work with threads directly in business applications, they’re used all the time in Java frameworks. As an example, frameworks that process a large volume of information use threads to manage data. Manipulating threads or CPU processes concurrently improves performance, resulting in faster, more efficient programs.

This article introduces you to some basics of traditional Java threads and thread execution in the Java virtual machine. See the InfoWorld introduction to Project Loom to learn about virtual threads and Java’s new structured concurrency model.

Find your first thread: Java’s main() method

Even if you’ve never worked directly with Java threads, you’ve worked indirectly with them because Java’s main() method contains a main Thread. Anytime you’ve executed the main() method, you’ve also executed the main Thread.

Studying the Thread class is very helpful for understanding how threading works in Java programs. We can access the thread that is being executed by invoking the currentThread().getName() method, as shown here:

<code>
public class MainThread {

    public static void main(String... mainThread) {
        System.out.println(Thread.currentThread().getName());
    }

}
</code>

This code will print “main,” identifying the thread currently being executed. Knowing how to identify the thread being executed is the first step to absorbing thread concepts.

The Java thread lifecycle

When working with threads, it’s critical to be aware of thread state. The Java thread lifecycle consists of six thread states:

  • New: A new Thread() has been instantiated.
  • Runnable: The Thread‘s start() method has been invoked.
  • Running: The start() method has been invoked and the thread is running.
  • Suspended: The thread is temporarily suspended, and can be resumed by another thread.
  • Blocked: The thread is waiting for an opportunity to run. This happens when one thread has already invoked the synchronized() method and the next thread must wait until it’s finished.
  • Terminated: The thread’s execution is complete.
A diagram showing the six stages of the Java thread lifecycle. Rafael Chinelato Del Nero

Figure 1. The six states of the Java threads lifecycle

There’s more to explore and understand about thread states, but the information in Figure 1 is enough for now.

Extending a Thread class

At its simplest, concurrent processing is done by extending a Thread class, as shown here:

<code>
public class InheritingThread extends Thread {

    InheritingThread(String threadName) {
        super(threadName);
    }

    public static void main(String... inheriting) {
        System.out.println(Thread.currentThread().getName() + " is running");

        new InheritingThread("inheritingThread").start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running");
    }
}
</code>

Here, we’re running two threads: the MainThread and the InheritingThread. When we invoke the start() method with the new inheritingThread(), the logic in the run() method is executed.

We also pass the name of the second thread in the Thread class constructor, so the output will be:

<code>
main is running.
inheritingThread is running.
</code>

The Runnable interface

Rather than using inheritance, you could implement the Runnable interface. Passing Runnable inside a Thread constructor results in less coupling and more flexibility. After passing Runnable, we can invoke the start() method exactly like we did in the previous example:

<code>
public class RunnableThread implements Runnable {

    public static void main(String... runnableThread) {
        System.out.println(Thread.currentThread().getName());

        new Thread(new RunnableThread()).start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

}
</code>

Non-daemon vs. daemon threads

In terms of execution, there are two types of threads:

  • Non-daemon threads are executed until the end. The main thread is a good example of a non-daemon thread. Code in main() will always be executed until the end, unless a System.exit() forces the program to complete.
  • A daemon thread is the opposite, basically a process that is not required to be executed until the end.

Remember the rule: If an enclosing non-daemon thread ends before a daemon thread, the daemon thread won’t be executed until the end.

To better understand the relationship of daemon and non-daemon threads, study this example:

<code>
import java.util.stream.IntStream;

public class NonDaemonAndDaemonThread {

    public static void main(String... nonDaemonAndDaemon) throws                        InterruptedException {
        System.out.println("Starting the execution in the Thread " +      Thread.currentThread().getName());

        Thread daemonThread = new Thread(() ->      IntStream.rangeClosed(1, 100000)
                .forEach(System.out::println));

        daemonThread.setDaemon(true);
        daemonThread.start();

        Thread.sleep(10);

        System.out.println("End of the execution in the Thread " +    
                                           Thread.currentThread().getName());
    }

}
</code>

In this example I’ve used a daemon thread to declare a range from 1 to 100,000, iterate all of them, and then print. But remember, a daemon thread won’t complete execution if the non-daemon’s main thread finishes first.

The output will proceed as follows:

  1. Start of execution in the main thread.
  2. Print numbers from 1 to possibly 100,000.
  3. End of execution in the main thread, very likely before iteration to 100,000 completes.

The final output will depend on your JVM implementation.

As you can see, threads are unpredictable.

Thread priority and the JVM

It’s possible to prioritize thread execution with the setPriority method, but, again, how it’s handled depends on the JVM implementation. Linux, macOS, and Windows all have different JVM implementations, and each will handle thread priority according to the defaults.

The thread priority you set does influence the order of thread invocation, however. The three constants declared in the Thread class are:

<code>
     /**
    * The minimum priority that a thread can have.
     */
    public static final int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;
</code>

Try running tests on the following code to see what execution priority you end up with:

<code>
public class ThreadPriority {

    public static void main(String... threadPriority) {
        Thread moeThread = new Thread(() -> System.out.println("Moe"));
        Thread barneyThread = new Thread(() -> System.out.println("Barney"));
        Thread homerThread = new Thread(() -> System.out.println("Homer"));

        moeThread.setPriority(Thread.MAX_PRIORITY);
        barneyThread.setPriority(Thread.NORM_PRIORITY);
        homerThread.setPriority(Thread.MIN_PRIORITY);

        homerThread.start();
        barneyThread.start();
        moeThread.start();
    }

}
</code>

Even if we set moeThread as MAX_PRIORITY, we cannot count on this thread being executed first. Instead, the order of execution will be random.

A note about constants vs enums

The Thread class was introduced with the very first Java release. At that time, priorities were set using constants, not enums. There’s a problem with using constants, however: if we pass a priority number that is not in the range of 1 to 10, the setPriority() method will throw an IllegalArgumentException. Today, we can use enums to get around this issue. Using enums makes it impossible to pass an illegal argument, which both simplifies the code and gives us more control over its execution.

What to remember about Java threads

  • Invoke the start() method to start a Thread.
  • It’s possible to extend the Thread class directly in order to use threads.
  • It’s possible to implement a thread action inside a Runnable interface.
  • Thread priority depends on the JVM implementation.
  • Thread behavior also depends on the JVM implementation.
  • A daemon thread won’t complete if an enclosing non-daemon thread ends first.

Common mistakes with Java threads

  • Invoking the run() method is not the way to start a new thread.
  • Trying to start a thread twice will cause an IllegalThreadStateException.
  • Avoid allowing multiple processes to change the state of an object.
  • Don’t write program logic that relies on thread priority (you can’t predict it).
  • Do not rely on the order of thread execution–even if you start a thread first, there is no guarantee it will be executed first.

Take the Java threads challenge!

You’ve learned just a few things about Java threads, so let’s try a Java challenge to test what you’ve learned.

<code>
public class ThreadChallenge {
    private static int wolverineAdrenaline = 10;

    public static void main(String... doYourBest) {
        new Motorcycle("Harley Davidson").start();

        Motorcycle fastBike = new Motorcycle("Dodge Tomahawk");
        fastBike.setPriority(Thread.MAX_PRIORITY);
        fastBike.setDaemon(false);
        fastBike.start();

        Motorcycle yamaha = new Motorcycle("Yamaha YZF");
        yamaha.setPriority(Thread.MIN_PRIORITY);
        yamaha.start();
    }

    static class Motorcycle extends Thread {
        Motorcycle(String bikeName) { super(bikeName); }

        @Override public void run() {
            wolverineAdrenaline++;
            if (wolverineAdrenaline == 13) {
                System.out.println(this.getName());
            }
        }
    }
}
</code>

What do you think will be the output of this code? Here are the options:

A. Harley Davidson
B. Dodge Tomahawk
C. Yamaha YZF
D. Indeterminate

Solving the challenge

In the above code, we created three threads. The first thread is Harley Davidson, and we assigned this thread the default priority. The second thread is Dodge Tomahawk, assigned MAX_PRIORITY. The third is Yamaha YZF, with MIN_PRIORITY. Then we started the threads.

To determine the order the threads will run in, you might first note that the Motorcycle class extends the Thread class, and that we’ve passed the thread name in the constructor. We’ve also overridden the run() method with a condition: if (wolverineAdrenaline == 13).

Even though Yamaha YZF is the third thread in our order of execution, and has MIN_PRIORITY, there’s no guarantee that it will be executed last for all JVM implementations.

You might also note that in this example we set the Dodge Tomahawk thread as daemon. Because it’s a daemon thread, Dodge Tomahawk may never complete execution. But the other two threads are non-daemon by default, so the Harley Davidson and Yamaha YZF threads will definitely complete their execution.

To conclude, the result will be D: Indeterminate. This is because there is no guarantee that the thread scheduler will follow our order of execution or thread priority.

Remember, we can’t rely on program logic (order of threads or thread priority) to predict the JVM’s order of execution.

Video challenge! Debugging variable arguments

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video you can follow along while I debug and explain the thread behavior challenge:

Learn more about Java

Copyright © 2024 IDG Communications, Inc.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button
WP Twitter Auto Publish Powered By : XYZScripts.com
SiteLock