We’ve covered plenty of different concepts in our Learn to Code series — in fact, enough for you to start writing your own Java console and graphical user interface (GUI) apps.
But those apps will contain just a single stream or ‘thread’ of code statements. For most basic apps, that’s fine.
However, single-threaded apps involving lots of computation can run into trouble and that’s where learning how to use threads can make your apps more responsive and less affected by computation-heavy algorithms.
What is multi-threading?
Put very simply, multi-threading is the process of coding several tasks or threads to run simultaneously.
When you run a basic Java GUI app, the first thing to run is the class’ main() method, which is run in the standard ‘main’ thread. But any code that’s launched from a GUI button action, for example, is run in what’s known as the Event-Dispatching Thread (EDT).
The problem is, that’s the same thread that the GUI components themselves are activated from.
So what happens? Let’s say you’re writing an algorithm-heavy app, calculating Pi to four-trillion decimal places using the Chudnovsky formula.
You can do nothing and just run this code from within the EDT, but while it’s doing those calculations, all of the GUI components also in that thread will lock up. Once the Pi code gets going, no GUI code can run until the PI code completes because they’re in the same thread.
But if you add in a second thread and hive off the calculations there, the Event-Dispatching Thread (and the GUI components inside) will continue to function normally.
Probably at this point, it’s worth just making sure we don’t confuse multi-
threading with multi-core CPUs. You don’t need a multi-core CPU to run a multi-threaded app.
Apps with multiple threads can obviously run on multi-core CPUs, but they can on a single-core CPU as well.
What happens on single-core chips is the core time-shares or multiplexes the threads so that each thread gets its turn on the CPU.
The downside is that the multi-threaded app will likely run slightly slower than the single-threaded version on a single-core CPU because of the overheads in time-sharing those multiple threads.
The other side of that coin is that a single-threaded app stays a single-threaded app regardless of whether you have a single- or multi-core CPU.
Windows has support for multi-core CPUs, but, using our Pi example above, if you code your algorithm to do its heavy lifting on the EDT, Windows can’t magically step in and separate those parts — you have to code it to happen.
Thankfully, Java makes it quite easy to implement multiple threads. Like just about everything else in Java, threads are objects, so they adhere to normal Java coding rules.
To begin, you create your task as a Java class as before, but you implement what’s known as the Runnable interface.
In Java, an ‘interface’ is a way of applying a set of common characteristics to different classes. For example, you could create a ‘computer’ interface that applies to smartphones, tablets, servers, desktops, notebooks and so on.
The ‘implements Runnable’ extension marks the class as runnable inside a thread, it also adds a new ‘run()’ method. Once the class is established, you then create a new Runnable object that is a new instance of that class.
Next, you create a new Thread object from the Runnable object using the Thread class. Finally, to run the thread, you implement the Thread.start() statement.
It’s actually easier in practice than it sounds, so let’s take a look at an example.
Grab the source code for this masterclass from our website here.
Unzip the file and load the SimpleThread.zip file into the NetBeans integrated development environment (IDE) by launching the IDE and selecting File > Import Project > From Zip.
In this example, we create three threads, each printing the numbers 0 to 4 to the console. We start by creating three static classes that implement the Runnable interface — PrintThread1, PrintThread2 and PrintThread3.
They’re all identical, except in the System.out.println() statement, where each thread prints a different ‘T’ number to indicate the thread. (Yes, there are more compact ways to write this code, but we’re just going for simplicity here — shortcuts come later.)
Notice here that all three runnable classes implement the ‘run()’ method. You must use this method, otherwise the thread won’t work — and in fact, NetBeans will demand it auto-completes the class by including the run() method if you don’t.
After that, we code the app’s main() method. Here, we start by creating Runnable objects of each ‘PrintThread’ class (objRun1, objRun2 and objRun3).
These are then passed to three new Thread objects (thread1, thread2, thread3) using the Thread class. And finally, we launch each thread with the start() command.
Not using Thread.run()?
But why use Thread.start() when the code is sitting in Thread.run()? Well, you could launch Thread.run(), but it would run in the existing ‘main’ method.
If you want your run() code to have its own thread, you must use the Thread.start() statement.
Now, sure, this seems like a dopey example, but it still highlights a very useful point. Run the code a few times and note the output each time.
As we’ve shown, you should find the order of the thread output in the console changes every time. You might get a couple of iterations from Thread 1, followed by Thread3, then Thread2, a few more from Thread3 and so on until all thread tasks have been completed.
This happens because launching multiple threads in a particular order does not guarantee the order in which those threads are executed or completed.
But once a thread is completed, Java closes it and it no longer exists.
Bottom line, all threads will eventually complete their required tasks, but how and when they do depends on what’s known as ‘thread scheduling’.
This is true when all threads have the same level of importance or ‘priority’, but you can also change the priority of individual threads.
An ambulance with lights flashing and siren blaring has priority over other road traffic — that’s an example of maximum priority and you set thread priority via the ‘thread.setPriority()’ method.
It’s determined by the parameter you pass to the method and can be either a Java constant or an integer from 1 to 10.
The three Java constants are Thread.MIN_PRIORITY, Thread.NORM_PRIORITY and Thread.MAX_PRIORITY, but if using the integer option, the higher the number, the higher the thread’s priority in the queue.
However, like most things, it’s never quite that simple. There are two issues — first, if you have more CPU cores available than threads to run, chances are all threads will run in the first instance regardless of the thread priority setting.
The priority setting only works if there’s a thread blockage — if there’s no blockage, there’s no need to prioritise.
The second issue is that database giant Oracle, the current custodian of Java, even suggests you don’t rely on priority settings because the implementation is platform-dependent.
That’s another way of saying not every operating system implements this priority option the same way, and if your aim is for your app to work on any platform, using setPriority() might cause you grief.
You can try this yourself. Load up BasicThreadPriority.zip into NetBeans from the same source zip file as you did with the previous example.
First thing to note is that rather than having three essentially identical Runnable classes, this time we have one Runnable class and implement a constructor that allows us to pass a thread number into it. We then use that class as the basis for our four threads.
We’re only interested in when the thread finishes here, so we forego the System.out.println() statements until the end.
The run() method has two for-loops — one of 50,000-count and another of 500,000,000 that’s commented out.
But the key thing in the main() method is that of the four threads created, Thread2 is set for maximum priority and Thread1 minimum priority.
Run the code as is with the 50,000-count for-loop and provided you’re running a fairly-recent Windows system, you should see the thread completion order is mostly random — Thread2 doesn’t always finished first, despite having maximum priority set.
Why does the size of the for-loop count make a difference? If the thread runtime is too short, the first thread to run could well be completed before the next one actually begins.
The longer the runtime execution, the more likelihood those threads will all be on the go at once and priority settings can be utilised.
If you’re wondering if Intel’s HyperThreading counts as extra cores to run threads on, the answer is not exactly, but it apparently does help.
Finally, we’ll look at an example of a key benefit for using threads — to stop GUI lock-up.
Remember before we talked about calculating Pi to four-trillion decimal places? We’ll do it to a few decimals here using a simple zeta function. Again, not a terribly useful app in itself, but perfect for showing how threads can keep your GUI moving.
In this example, we’re coding it two ways — the first, by running the calculation (a big 50 million-count for-loop) in the default event-dispatching thread; the second, by running it in a separate thread.
How to build your Java apps
But you can’t run this example in the NetBeans IDE — we actually need to build the code as a .jar (Java ARchive) runtime app to see it work properly.
We’ve not done this before, but it’s dead-easy and the resulting .jar file is what you’d normally distribute.
With the BasicThreadPi.zip file loaded into the NetBeans IDE main panel, press F11 and click the ‘Clean and Build’ button.
Now open up the \NetBeansProjects\BasicThreadPi folder in Windows Explorer under your Documents folder, go to the ‘dist’ subfolder and you should see a ‘BasicThreadPi.jar’ file. Double-click on the file and it’ll launch the app.
If you get this far, congratulations — you’ve just built your first Java run-time distributable file.
Meanwhile, if you click on the app’s ‘Calculate Pi (single thread)’ button, the app will begin its 50 million iterations of a for-loop to calculate Pi. But in doing so, it’ll temporarily lock up the GUI, as shown by the ‘single thread’ button not releasing.
Again, the reason is those iterations are happening on the same thread as the GUI execution. On my 3GHz quad-core Intel Core i5 2300 desktop, it takes around five seconds to complete the calculation, as noted by our timing code in the jTextArea object.
If you look at the source code under jButton1ActionPerformed(), you’ll see the first thing we do is append the text line ‘this text will appear AFTER calculation is completed’ to the jTextArea1 object, then we run the calculatePi() method.
But sure enough, if you watch the output, you won’t actually see that text until after the Pi calculation is completed. That’s another example of GUI lockup, right there.
Now if you click on the ‘dual thread’ button, we’re running the same calculatePi() method as before, but this time, inside a second thread.
Further, the text line appears before the calculation ends, even though, if you look at the jButton2ActionPerformed() method, its coded after launching the calculation method. Not only that, you can even type in the jTextArea while the calculations continue in the background.
That’s the benefit of using a second thread — you hive off the CPU-intensive stuff to that second thread, allowing the GUI thread to remain responsive to the user.
Threads aren’t perfect
Now before you get the impression that threads are perfect in every way, they’re not. Running multiple threads adds overheads in performance and resources that don’t occur with single-threaded apps.
Run a multi-threaded app on a single-core CPU, for example, and chances are the overall processing performance will be slightly slower than the single-threaded version because of those overheads.
However, multithreaded apps on multi-core CPU’d systems should be a win-win (subject to your code being suitable).
Aside of any performance differences, the GUI should remain responsive on apps using correct multithreading, regardless of the number of cores available.
Just a taste
There are volumes dedicated to the theory of multi-threading code, so we can only hope to give you a brief taste here of how threads work in Java and what they can do.
Hopefully, you can see that, in the right context, threads can help your algorithm-intensive apps remain GUI-responsive and in the right circumstances, gain extra performance available from multi-core systems.