At the end of November, we’ll be migrating the Sematext Logs backend from Elasticsearch to OpenSearch

JVM Threads

December 9, 2021

Table of contents

What Is a Thread in Java?

A Java thread is the execution path in a program. Everything that runs in Java is run in threads. Every application in the JVM world has threads, at least one, even if you don’t call it explicitly. It all starts with the main method of your code, which is run in the main application thread. And of course, all the threads created in the code are really created by the Java Virtual Machine itself and managed by it.

Java Thread Life Cycle

There is a particular life cycle that each thread goes through inside the Java Virtual Machine. Those steps are:

  • new – a class implementing the Thread has been created,
  • runnable – the start() method of the thread has been called,
  • running – the start() method of the thread is being executed,
  • suspended – the thread execution is waiting to be resumed by another thread,
  • blocked – the thread execution is blocked because of synchronization, for example, synchronized block or variable,
  • terminated – the thread execution is complete.

Java provides methods for manual thread life cycle control like stop, suspend, or resume, but they are considered deprecated and will be removed in future Java versions. Using them is considered deadlock prone, so keep that in mind if you would like to use them inside your code.

How Many Threads Can a JVM Handle?

There is no straight and easy answer to such a question. How many threads can a JVM handle depends on a lot of factors. You need to consider: the available memory, the configuration of the operating system processing power of your machine, how complicated is the code that is executed inside the thread, and many, many more. One thing you can be sure of – the more resources you throw at your code, the more threads it will be able to process if your operating system allows.

Why Use Java Threads

There are several reasons for using multithreading in Java.

Parallel Processing

The main thread in your Java application will sequentially execute your instructions until there is no more code to run or the execution of your program is stopped on purpose, for example, by using the exit method of the Runtime class.

Such execution is good, but it doesn’t leverage the capabilities of the modern hardware – the possibility to run multiple operations in parallel. That’s why your JVM code can create and run new, multiple threads whenever you feel that your program will benefit from parallelization of execution.

Have a look at the following piece of code:

File file = new File("some_big_file");
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
    storeDataFromLine("some_big_file", line);
}
reader.close();
file = new File("yet_another_big_file");
reader = new BufferedReader(new FileReader(file));
while ((line = reader.readLine()) != null) {
    storeDataFromLine("yet_another_big_file", line);
}
reader.close();

You can visualize the execution of that code as follows:

The problem with the above code is simple – it first reads one file, and the second file is waiting to be read even if the machine running the code has enough processing power to read the second file simultaneously. That means that you are losing performance. By using multiple threads, you can change the code to be executed as follows:

The above picture shows a very simplified view of modern software. You would like your code to be efficient and handle thousands of users at the same time. This is where threads help as well. By using multiple threads, your code can wait for one user’s input and process other users simultaneously. If you use a single thread, it will force the code to wait for the user input to be able to process more – not an efficient solution.

Better Hardware Utilization

Multi-threaded applications can leverage the architecture of modern hardware. Your computer, the machine your code will be running, or even a mobile device – each has multiple CPU cores that can run multiple operations at the same time. You can display a video to your user using one thread and read the data from a remote location using another thread. You can split large portions of data into smaller pieces and run the same processing over the created chunks – speeding up the process by parallelizing it.

Handling Multiple Requests at the Same Time

Finally, by using Java threads, you can write server-side software to handle multiple user requests simultaneously without one needing to wait for the end of request processing of another user.

In short, without multithreaded applications, we wouldn’t see fast and efficient applications handling hundreds or thousands of users per second, real-time rendering, and many, many more.

The Cost of Using Threads in Java

Threads don’t come for free. They come with additional overhead, such as the memory needed to handle them and the extra CPU resources to coordinate them. You can even make your application work worse if you just use too many threads. The CPU needs to dedicate its time to process threads, and if there are too many threads, your CPU will need to do a lot of context switching.

The switching of context is a process of storing the state of the thread so that it can be reused later when the CPU can process it. Such operation is something that the CPU is doing all the time, but it is also not free. That means that the more threads you have, the more context switching will happen and the more your performance will suffer. Up to a point where adding more threads doesn’t increase performance but rather decreases. That’s why you should keep an eye on the number of threads that your application uses.

Creating Threads

Creating a new thread in Java is not complicated. There are two ways you can do this:

Extending the Thread class and implementing the run method. For example like this:

class MyAwesomeThread extends Thread {
 @Override
 public void run() {
   // your code goes here
 }
}

Thread Class vs Runnable Interface

The Thread class implements the Runnable interface. The rule of thumb is that if you only plan on overwriting the run method, you should implement the Runnable interface and not create the subclass of the Thread class itself. However, if you would like to leverage the functionality of the Thread class and be able to control the life cycle of your thread, then extending the Thread class is the way to go.

Starting Java Threads

Starting the threads themselves is not complicated either. Now extend the above example and include some waiting code to simulate waiting for users input so that our thread class looks as follows:

class MyAwesomeThread extends Thread {
  private int number;
  public MyAwesomeThread(int number) {
    this.number = number;
  }
  @Override
  public void run() {
    System.out.println("Thread " + number + " started");
    try {
      Thread.sleep((long)((Math.random() * 100) + 100));
    } catch (Exception ex) {}
    System.out.println("Thread " + number + " ended");
  }
}

This time it will print a message with the thread number, sleep for some random milliseconds and print the information about ending the Java thread.

The above code can be run as follows:

public static void main(String[] args) throws Exception {
  for (int i = 1; i <= 10; i++) {
    MyAwesomeThread thread = new MyAwesomeThread(i);
    thread.start();
  }
}

The above code uses a loop to create 10 instances of the MyAwesomeThread class and executes its start method. The start method is responsible for starting the thread.

The output of the above code will vary because of the randomness. In our case, it was the following:

Thread 1 started
Thread 3 started
Thread 2 started
Thread 4 started
Thread 5 started
Thread 6 started
Thread 7 started
Thread 8 started
Thread 9 started
Thread 10 started
Thread 3 ended
Thread 2 ended
Thread 10 ended
Thread 6 ended
Thread 7 ended
Thread 4 ended
Thread 5 ended
Thread 1 ended
Thread 9 ended
Thread 8 ended

As you can see in the output, the threads ended in a different order than they started. And, of course, this is what the code is aiming for.

Thread Control – Executors

What if you would like to have only 5 threads running at the same time and the next thread after that to be held until there is “space”?

A standard thread pool scenario – you would like a finite amount of threads to be running at the same time. You could add listeners and extend the code, but that would complicate it. Instead, you can use executors. The modified code would look as follows:

public static void main(String[] args) throws Exception {
  ExecutorService executorService = Executors.newFixedThreadPool(5);
  for (int i = 1; i <= 10; i++) {
    MyAwesomeThread thread = new MyAwesomeThread(i);
    executorService.execute(thread);
  }
}

Instead of manually starting the threads, you use the ExecutorService and one of its implementations – the one using the fixed number of threads. This means that the code will execute up to 5 threads at the same time and will wait with starting a new thread until there is a place in the pool for that. All of that is controlled by the executor itself and you don’t have to complicate the code.

There are also other implementations of executors. You have the cached thread pool executor that caches Runnable instances in the queue. You have a single thread executor that uses a single thread for execution. No matter what implementation your code uses, they all provide ways to observe when the executor finishes its job. The submit method of the executor returns a Future class, which gives you the result of the thread execution once it completes.

Java Thread Name

In Java, each thread has its own name. They can be used to easily identify the thread and give the context. Let’s assume you are using one of the popular frameworks for logging in Java and your code does extensive calculation on the account balance. You can name the thread balance_calculation and your logging library will be able to display that so you can easily identify from which part of the code the message comes.

By default, if you don’t provide the name for your threads their names will look like Thread-123, etc. So the keyword Thread followed by a hyphen and an increasing integer from 0. This is not very informative and it is suggested to use a dedicated thread name.

So how would you give a name to a thread? We do that by using the setName method of the Thread class. For example:

...
Thread thread = new MyAswesomeThread();
thread.setName(“my_awesome_thread”);
...

Why Monitor Java Threads?

Monitoring Java threads – their number, state, and life cycle – is key to understanding the performance of your applications. The live view of the threads’ state and the place in the code that they are processing at the moment is important and can be invaluable in diagnosing application issues.

Threads in the blocked state waiting for data reading from disk can point to issues with the storage performance, while threads blocked waiting for a resource that is shared between them point to a deadlock and problems in the code itself. That’s why it is important to monitor the threads running inside your Java Virtual Machine.

Finally, the number of Java threads, especially ones that perform complicated computations, is crucial for performance. As each thread requires additional resources, you should consider it during the JVM performance tuning. Some applications even allow you to control the number of threads that can be concurrently running, so keep that in mind.

Java monitoring tools help you gaining valuable insights into the performance of you Java applications providing information such as:

  • the memory pool size,
  • the memory pool utilization,
  • heap and off-heap memory,
  • the number of threads running,
  • the number of opened files,
  • the maximum opened files allowed.

Thread Concurrency and Synchronization

One of the potential issues you may face as the developer working on the code is Java thread concurrency and thread synchronization. One of the simplest examples is accessing the same variable from multiple threads – like a counter responsible for tracking the number of errors. For example like this:

class MyAwesomeExample extends Thread {
  private int errorCount;
  @Override
  public void run() {
    try {
      // some code
    } catch (Exception ex) {
      errorCount++;
    }
  }
}

The above code is a bit faulty even though it seems like a very simple one. The problem is that the errorCount variable may be accessed by multiple threads at the same time. That means that one Java thread can overwrite the value written by another thread making the value not correct. This is a major issue – think about use cases like banking, when multiple users can access the account balance and schedule wires.

To avoid that kind of issue we can either use atomic variables, but that is not an option in some cases – especially when updating more complicated objects. The second option is to use the synchronized keyword, which prevents multiple threads from accessing the same variable, for example like this:

class MyAwesomeExample extends Thread {
  private violatile int errorCount;
  @Override
  public void run() {
    try {
      // some code
    } catch (Exception ex) {
      incrementErrorCount();
    }
  }
  protected synchronized void incrementErrorCount() {
    errorCount++;
  }
}

Java Thread Monitoring with Sematext

Monitoring Java Threads can’t be simpler than with Sematext Monitoring and its Java Monitoring tool. Out of the box, you get not only the information about the threads created and running inside your Java Virtual Machine, but it helps you to monitor key JVM metrics such as garbage collection, memory usage, and more.

The solution is part of Sematext Cloud, a full-stack monitoring platform that offers end-to-end visibility of your IT infrastructure. It can provide insight into your garbage collection logs and, along with the Java Monitoring tool, will bring the observability of your JVM-based applications to the next level.

Java Logging Basics: Concepts, Tools, and Best Practices

Imagine you're a detective trying to solve a crime, but...

Best Web Transaction Monitoring Tools in 2024

Websites are no longer static pages.  They’re dynamic, transaction-heavy ecosystems...

17 Linux Log Files You Must Be Monitoring

Imagine waking up to a critical system failure that has...