Sunday, September 25, 2011

Multithreading with Java and Swing

It was seven years ago when Intel released the world's first dual-core processor for the consumer market.  Back then, concurrency was completely new and anyone who knew how to program for multiple cores was considered a hotshot programmer.  Physical limitations (mainly heat and power leakage) limited individual cores to operating frequencies of ~3.5 GHz.  Their solution?  Die shrinks and multiple cores.

Today, dual-core and quad-core processors have become mainstream in mobile electronics and home computers.  As a result, concurrency skills are no longer considered a luxury, but a requirement for any serious developer.  The rest of this post will detail my first recent experience with multithreaded programming using Java and Swing.

Applications that have graphical user interfaces (GUIs) are naturally multithreaded.  Usually you have one thread that draws the GUI, and another thread that runs the logic behind the scenes.  By splitting these two tasks into separate threads, you can get a nice performance boost if each thread runs on a separate core.  The GUI is quick n' snappy.  The user is happy.  And the developer is happy, well, for the most part if the job gets done correctly (issues of race conditions, deadlocks, and starvation may be covered in a future post).

Moving on, Java SE6 provides two convenient libraries for getting our hands dirty with GUI-based multithreading, Swing and Thread.  I used these libraries to whip up an application to spawn multiple threads, assign a priority to each thread, perform a computationally intensive calculation in a loop, and report the average time for each thread to complete a calculation.

Screenshot of Swing application

The workflow for my Swing application is:
  1. Create the GUI elements like the frame, start button, dropdown lists, panels, checkboxes, and labels.
  2. Attach an event listener to the start button.
  3. Set the frame to visible with frame.setVisible(true).
  4. Handle an event in the actionPerformed method when the user clicks the start button.  The event handler checks the number of threads that the user wants to start.  Then it creates the threads with the user-selected thread priority.
  5. Calculate the sum of the first 1000 cosines for 120 times in a loop for 3 minutes on each thread.
  6. After each iteration of the loop, calculate the average amount of time it took (in milliseconds) to perform the calculation and update the GUI.
So how does this relate to GUI-based multithreading?  Swing incorporates a separate thread called the Event Dispatch Thread (EDT).  It is the thread that is responsible for handling all events and updating the GUI.  This means that you, as a programmer, cannot manually update the GUI yourself in the main thread or a runnable thread that you have created yourself.  To update a GUI component properly, you must either place your code in the actionPerformed method (because it runs on the EDT), or place your code in another class and invoke it using the SwingUtilities.invokeLater method (which tells it to run on the EDT).

The main idea is to break the program into threads that perform background computation and thread(s) that update the GUI (the EDT does this for us).  Since you want to keep the GUI responsive, and the EDT is responsible for updating the GUI, you don't want to have long-running pieces of code on the EDT.  Instead, have the EDT spawn off new threads and run your (slower) code in these new threads.  Proper usage of these concepts can be seen in my program and source code here.  Compile with "javac" and run with "java ManyPanels 0".

I ran an experiment to see how thread priority scheduling works on different operating systems.  On a Windows machine with 2 cores and a Mac machine with 2 cores, I ran 10 threads with priorities ranging from 1 to 10.  The first number under each panel indicates the average amount of time (in milliseconds) it took to get through one iteration, and the second number in parentheses indicates the number of iterations that the thread was able to execute in 3 minutes.

Windows.  Thread priorities are all over the place.  A thread with priority 1 should not run faster than a thread with priority 8.
Mac.  Thread priorities are in order, with 1 running the slowest and 10 running the fastest.
The results show that thread priority scheduling in the JVM and Windows is completely unreliable, while thread priority scheduling in the JVM and Mac works as expected.  This is food for thought for developers who program with thread priorities.