April 26, 2020

Thread Magic Tricks: 5 Things You Never Knew You Can Do with Java Threads

Table of Contents

What are some of the least known facts and use cases for Java threads?

Some people like mountain climbing, others like sky diving. Me, I like Java. One of the things I love about it is that you never stop learning. The tools you use on a daily basis can often reveal a whole new side to them, with methods and interesting use cases you haven’t had a chance to see yet. Like threads for example. Actual threads. Or better put, the Thread class itself. Concurrent programming never stops posing challenges when we’re dealing with high scalability systems, but now we’ll talk about something a bit different.


In this post you’ll see some of the lesser-known yet useful techniques and methods that threads support. Whether you’re a beginner, advanced user or an expert Java developer, try to see which of these you already know and what comes off as new to you. Is there something else about threads you feel worth mentioning? I’d love to hear about it in the comments below. Let’s get started.

Beginner

1. Thread names

Each thread in your app has a name, a simple Java String that’s generated for it when the thread is constructed. The default name values go from “Thread-0” to “Thread-1”, “Thread-2” and so on. Now comes the more interesting part – Threads expose 2 ways you can use to set their names:
1. The thread constructors, here’s the simplest one:


2. The thread name setter:

Right, thread names are mutable. So other than setting a custom name when we’re instantiating them, we can change it during runtime. The name field itself is set as a simple String object. This means it can go up to 2³¹-1 characters long (Integer.MAX_VALUE). More than enough I’d say. Please note that this name doesn’t act like a unique ID, so threads can share the same name. Also, don’t try passing null as a name unless you want an exception to be thrown (“null” is ok though, I’m not judging!).

Using thread names for debugging

So now that you have access to thread names, following some naming conventions of your own could make your life much much easier when something bad happens. “Thread-6” sounds a bit heartless, I’m sure you can think of a better name. Couple this with a self-assigned transaction ID when handling user requests, append it to the thread’s name and you’ve considerably cut down your error-solving time.
A good practice to keep here is making sure you generate a UUID at every thread’s entry point to your app, and keep it consistent as the request travels between your nodes, processes and threads. Let’s take a look at this example, one of the worker threads in a certain thread pool hangs for too long. You run jstack to take a closer look and then you see this:

“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]
Ok, “pool-1-thread-1”, why so serious? Let’s get to know you better and think of a more suitable name:
[java]
Thread.currentThread().setName(Context + TID + Params + current Time, …);
[/java]

Now when we run jstack again, things look much brighter:

Queue Processing Thread, MessageID: AB5CAD, type:
AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956,
Start Time: 30/12/2014 17:37″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]

We know what the thread is doing, when it got stuck, and we also have the transaction ID that started it all. You can retrace your steps, reproduce the error, isolate and solve it.

2. Thread Priorities

Another interesting field threads have is Priority. A thread’s Priority is a value between 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), and the default value for your main thread is 5 (NORM_PRIORITY). Each new thread gets the priority of its parent, so if you’re not playing with it manually, all your thread priorities are probably set to 5. This is also an often overlooked field of the Thread class, and we can access and manipulate it through the methods getPriority() and setPriority(). There’s no way to set this in the thread constructor.

Who Needs Priorities Anyhow?

Of course not all threads are created equal, some require immediate attention from your CPU while others are just background tasks. Priorities are used to signal that to the OS thread scheduler. At Harness, where we develop an error tracking and analysis tool, the thread that handles new exceptions for our users gets a MAX_PRIORITY, while threads that handle tasks like reporting new deployments are given a lower priority. One might expect that threads with a higher Priority get more time from the thread scheduler working with your JVM. Well, that’s not always the case.


Each Java thread opens a new native thread on the OS level, and the Java priorities that you set are translated to native priorities in a different way for each platform. On Linux, you’ll also have to include the “-XX:+UseThreadPriorities” flag when running your app for them to be considered. With that said, thread priorities are still just recommendations that you provide. Compared to native Linux priorities, they don’t even cover the whole spectrum of values (1..99, and the effects of thread niceness that range between -20..20). The main takeaway is the importance of keeping your own logic that would ensure your priorities are reflected in the CPU time each thread gets, but it’s not recommended to rely solely on priorities.

Advanced

3. Thread Local Storage

This one is a bit different than the other creatures we talked about here. ThreadLocal is a concept that’s implemented off the Thread class (java.lang.ThreadLocal), but stores unique data for each thread. As it says on the tin, it provides you with Thread Local Storage, meaning you can create variables that are unique to each thread instance. Similar to the way you would have a thread name or priority, you can create custom fields that act as if they’re members of the Thread class. Isn’t that cool? But let’s not get too excited, there are some caveats ahead.

It’s recommended to create a ThreadLocal in one of two ways: Either as a static variable or part of singleton where it doesn’t have to be static. Note that it lives on the global scope, yet acts local to each thread that’s able to access it. Here’s an example of a ThreadLocal variable holding a data structure of our own for easy access:

[java]
public static class CriticalData
{
public int transactionId;
public int username;
}
public static final ThreadLocal<CriticalData> globalData =
new ThreadLocal<CriticalData>();
[/java]

Once we have a ThreadLocal in our hands, we can access it with globalData.set() and globalData.get().

Global? It Must Be Evil

Not necessarily. A ThreadLocal variable can keep a transaction ID. This can come in handy when you have an uncaught exception bubbling up your code. A good practice is to have an UncaughtExceptionHandler in place, which we also get with the Thread class but have to implement ourselves. Once we reach that stage, there aren’t many hints as to what actually got us there. We’re left with the Thread object and can’t access any of the variables that go us there as the stack frames shut down. In our UncaughtExceptionHandler, as the thread takes its last breaths, ThreadLocal is pretty much one of the only things we have left.
We can do something in the spirit of:

[java]
System.err.println(“Transaction ID ” + globalData.get().transactionId);
[/java]

And just like that we added some valuable context to the error. One of the more creative ways to use ThreadLocal is by allocating a designated chunk of memory to be used as a buffer over and over by a worker thread. This can become useful depending on which side you’re on in the memory vs. CPU overhead tradeoff of course. That said, the thing to look out for is abuse of our memory space. ThreadLocal exists for a specific thread as long as it’s alive and will not be garbage collected unless you free it or the thread dies. So you better be careful when you use it and keep it simple.

4. User Threads and Daemon Threads

Back to our Thread class. Each thread in our app receives either a User or a Daemon status. In other words, a foreground or a background thread. By default, the main thread is a User thread and each new thread gets the status of the thread that created it. So if you set a thread as Daemon, all the threads it creates will be marked as daemon as well. When the only threads left running in your app are of Daemon status, the process closes. To play around, check and change a threads status we have the Boolean .setDaemon(true) and .isDaemon() methods.

When Would You Set a Daemon Thread?

You should change a thread’s status to Daemon when it’s not critical for it to end so the process could close. It takes off the hassle of closing the thread properly, stopping everything at once and let’s it end quickly. On the other hand, when there’s a thread that runs an operation that must end properly or else bad things will happen, make sure it’s set as a User thread. A critical transaction could be, for example, a database entry or completing an update that can’t be interrupted.

Expert

5. Java Processor Affinity

This part takes us closer to the hardware, where the code meets the metal. Processor affinity allows you to bind threads or processes to specific CPU cores. This means that whenever that specific thread executes, it would run exclusively on one certain core. Normally what would happen is that the OS thread scheduler would take on this role according to its own logic, possibly taking the thread priorities we mentioned earlier into account.

The bargaining chip here is the CPUs cache. If a thread would only run on one specific core, it’s more likely it will get to enjoy having all its data ready for it on the cache. When the data is already there, there’s no need to reload it. The micro-seconds you save can be put to better use and the code will actually run on that time, making better use of the allocated CPU time it got. While some optimizations do exist on the OS level, and the hardware architecture also has an important role of course, using affinity can eliminate the chance of a thread switching cores.

Since many factors are in play here, the best way to determine how processor affinity would affect your throughput is to embrace the habit of testing. While it may not always be significantly better, one of the benefits you might experience is a steady throughput. Affinity strategies can go down to a surgical level, depending on what there is to gain. The high frequency trading industry would be one of the places where these kind of things matter most.

Testing Processor Affinity

Java doesn’t have native support for processor affinity but that’s not the end of the story of course. On Linux, we can set a process affinity using the taskset command. Say we have a Java process running and we want to pin it to a specific CPU:
taskset -c 1 “java AboutToBePinned”
Or if its already running:
taskset -c 1 <PID>

Now, to get down to the thread level we’ll need to insert some new code. Luckily, there’s an open-source library that would help us do just that: Java-Thread-Affinity. Written by Peter Lawrey at OpenHFT, this is probably the most straightforward way to do this. Let’s see a quick example of pinning a thread, more of this is available on the library’s GitHub repo:
[java]
AffinityLock al = AffinityLock.acquireLock();
[/java]

And that’s it. More advanced options for acquiring the lock – taking into account different strategies for choosing the specific core – are available on GitHub.

Conclusion

We’ve seen 5 ways to look at threads: Thread names, thread local storage, priorities, daemon threads and affinity. Hope this helped shed a new light on the things you deal with on a daily basis, and would be glad to hear your comments! What other thread-handling methods could fit in?

Harness Service Reliability Management shows you when and why your code breaks in production. It detects caught and uncaught exceptions, HTTP and log errors, and gives you the code and variable state when they happened. Get actionable information, solve complex bugs in minutes. Installs in 5-min. Built for production.

Service Reliability Management