rafael_del nero
Java Developer

Thread behavior in the JVM

how-to
27 Jun 202411 mins
ConcurrencyCore JavaJava

The JVM does what it wants to do, so how can you predict the order of thread execution?

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, like Spring Batch, use threads to manage data. Manipulating threads or CPU processes concurrently improves performance, resulting in faster, more efficient programs.

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:


public class MainThread {

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

}

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.

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 you to solve this Java challenge.

Concurrent processing: Extending a Thread class

At its simplest, concurrent processing is done by extending a Thread class, as shown below.


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");
    }
}

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:


main is running.
inheritingThread is running.

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:


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());
    }

}

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 be 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:


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());
    }

}

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.

And that brings me to my next point: threads are unpredictable.

Thread priority and the JVM

It’s possible to prioritize thread execution with the setPriority method, but 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 its own defaults.

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


     /**
    * 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;

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


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();
    }

}

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.

Take the Java threads challenge!

You’ve learned just a little bit about threads, but it’s enough for this post’s Java challenge.

To start, study the following 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());
            }
        }
    }
}

What will be the output of this code? Analyze the code and try to determine the answer for yourself, based on what you’ve learned.

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

What just happened?

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.

In order 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 is equals to 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, 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:

Common mistakes with Java threads

  • Invoking the run() method to try to start a new thread.
  • Trying to start a thread twice (this will cause an IllegalThreadStateException).
  • Allowing multiple processes to change the state of an object when it shouldn’t change.
  • Writing program logic that relies on thread priority (you can’t predict it).
  • Relying on the order of thread execution–even if we start a thread first, there is no guarantee it will be executed first.

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 will always depend on the JVM implementation.
  • A daemon thread won’t complete if an enclosing non-daemon thread ends first.

Learn more about Java and Java threads

Exit mobile version