If you’ve been around for enough time you have certainly heard your share of complaints about Swing, which usually from it is too complex, the UI is ugly, to it is too slow. Why all the cursing against Swing? Most certainly my few years of experience have taught me to be careful when coding with Swing components, and even more so during the design phase of my application. But is the Swing framework really to blame for all the failures? If you actually follow the rules and best practices as documented by its designers, will you end up with a fast, usable and appealing client? I believe you will and this has been proven by quite a few clients out there, some of the best examples can be found at the Swing sightings page. In this post, I will try and go through the main challenges, and give a few pointers to solutions, when writing Swing applications.
On modern hardware, you need to be able to run things in parallel in order to have access to the computational resources of your environment. You usually have multiple CPUs or Cores, and you want to be able to use all the available cycles available to you. In Java, this is mostly done using threads, and lots of them (Take a thread dump of any of your Java applications and you will see that even though you didn't create any, the JVM already has quite a few of its own). Creating a single threaded application is still possible but it should be restrained to simple applications (You don't want to bother with multi threading for implementing Tic-Tac-Toe). Java was one of the first languages to fully embrace threading as part of its specification. You can create multi threaded code that is both correct and portable, however, as all things code, it is only correct if you code it right, which can prove to be a challenge (Regardless of the Java language, code executed by multiple threads is hard to get right). So how does Swing propose to help with this? Well, by making all access to Swing components single threaded... Wait a minute! You just said that to create an efficient application you need to run things in parallel, with like multiple threads and stuff??? How does this help? Well, in fact it doesn’t and that is why you need to be extra careful when writing applications using the Swing framework.
Let’s just pause a moment and see why the single threaded model. Ideally, you would like to be able to just spawn of threads each time there is a new action taking place in your client. That should make use of all the available CPU time we can get, right? First, spawning off threads is not free, so you don't want to do it each time the user hits the keyboard or moves the mouse. Second, you're likely to chase bugs until the end of time itself and then for a little while longer. Each time a thread accesses a shared resource you need to synchronize access to it to make sure that the resource remains consistent through the multiple accesses. Now that you have synchronized the access to all shared resources (or more likely all the ones you know about) you've opened the door for deadlocks and still haven't fixed race conditions occurring because some threads need to synchronize access across several resources. So basically the wild wild west of threading is likely to see a lot of unlawful behaviour and not be that efficient at all because of all the time spent synchronizing. AWT, the predecessor to Swing and its foundation actually tried to deliver a framework allowing concurrent access through multiple threads. Let's just say that Sun's bug database regarding AWT is unlikely to be empty of threading issues anytime soon. Swing tried to fix that by introducing a simple, yet so hard to enforce, rule that all Swing components must be accessed by a single thread, namely the Event Dispatch Thread (EDT).
So how is this done? Well quite simply, each time you access some Swing resource (both reading or changing), you need to package it inside a new Runnable that you pass on to the EventQueue, just like this:Runnable swingRunnable = new Runnable() {
public void run() {
// Doing some Swing stuff...
}
}
EventQueue.invokeLater(swingRunnable);
This plain sucks because, first these few lines of code will potentially be repeated over and over again in your code, this, however, in the minor annoyances category. Second, you only have one thread to execute all your code. This is a bit more critical; while the EDT is busy, clicking buttons and hitting the keyboard has no apparent effect, because your application cannot process these events until the current job on the EDT is done. This is a concern of perceived performance, most people don’t really mind looking at a progress bar (Actually, it can be quite fascinating), but looking at a frozen application will significantly increase their blood pressure. You don’t get things done faster with a progress bar, but the elapsed time somehow gets more acceptable. Thirdly, I sometimes forget to turn off the ironer, and while I might get away with it for my entire life, there's a chance that I will set my house ablaze some day. It is the same with Swing code, if you forget to put it on the EventQueue, it’s probably going to be okay, most of the time... However, some day you might receive a call from your customer at the hospital, asking why the application displayed the wrong amount of medication for that now deceased patient. Then, you’d wish your house went ablaze so you wouldn’t have had time to write that fatal line of code.
The first problem can be largely remedied be getting your design work right and structuring your code so that the Swing code is isolated from the rest of the code. If you end up having several invokeLater() calls inside a single method, it probably means you need to refactor your code. This might not always be possible with legacy code, in which case you might just have to live with it, you probably will have to live with worse things in your life as a code monkey.
The second problem is the single greatest cause of poorly performing Swing applications out there, some people have actually given up because of this, cursing the Swing framework for its slowness. So how do you fix this? Well several solutions exists more of less refined, but they all come from the same basic principle. You need to keep the amount of code executed on the EDT to a minimum and spawn as much processing as possible to other threads. Ideally, the code executed on the EDT only updates the state of the Swing components to reflect the results of the processing, not the processing in itself. If you do this, then your application would have a good perceived performance in most cases, and if you had some status reporting, like an hourglass or a progress bar, then you are almost certainly home safe. To help doing that several libraries and utility classes exist, the most famous is probably the SwingWorker, which is now part of the standard JDK since Java 1.6. This class allows you to easily split your code up into background processing code and Swing code. Other nice helper libraries include SPIN and the earlier Foxtrot. These two libraries decouple the actual data model from you Swing components. Looking forward you should pay attention to JSR 296, Swing Application Framework, which promises to include new concepts and utilities to ease threading concerns in Swing. So all is actually good in the fantastic world of Swing, well almost… The Swing framework actually has a few problems on its own, related to the infamous grey rectangle.
The grey rectangle problem is when some part of your application does not get repainted immediately and the region just shows up as a grey area, usually rectangular. This is actually caused by a deficiency in the design of Swing. Normally, the OS passes on events to applications for them to process the event and repaint part of their display if necessary. During that time, the OS is smart enough to realize that while processing the event, the application is not able to repaint itself should a new event force that, so the OS actually caches the current display of the application. Meaning that, even if you move another window on top of a busy application and move it away, the OS can use the cache to paint the busy application after the other window is moved away. The OS knows to use the cache because the application hasn’t returned from the last event that was passed to it. Swing on the other hand decouples the event processing from the OS, by placing the event on the EventQueue and returning control to the OS immediately. The OS has now no way to know that the application is still busy handling that event passed to it a while, and therefore does not use its cache to display the window. This problem is pretty much insolvable unless you change the fundamental design of Swing. A few things have been done to alleviate it over time, like in Java 1.6 with the introduction of double buffering on all components. All this means is that Swing repaints faster, meaning the grey rectangle is less likely to be noticeable. However, all these optimizations are not pointless, because if you actually keep the EDT mostly free, your application will plenty of time to repaint itself on a modern desktop.
The third problem is probably the most serious of all, because it is sneaky in its nature. It will remain hidden most of the time; you can successfully go through internal test, beta release, a few years of field test, until one day, it strikes in a place where you really did not want it to happen. Good design and implementation practices can help you avoid it to a certain extend, but even the best developers make mistakes (In fact, a good developer is often differentiated from a bad one, by the simple fact that he admits to making mistakes, and does something to catch them.) Static code analysis is unlikely to be able to detect this problem; this is in essence a runtime problem. Is my code running in the right thread? A few simple tricks, can allow you to detect violations to the rule. You can write a custom RepaintManager that will override the addDirtyRegion(), markCompletelyDirty() methods to check that these methods are being executed on the EDT. This can be simply done like this:public class CustomRepaintManager extends RepaintManager {
public void addDirtyRegion() {
if(!EventQueue.isDispatchThread()) {
throw new RuntimeException(“Running on the wrong thread!”);
}
super.addDirtyRegion();
}
}
This will detect all accesses that would cause a repaint on the wrong thread. This, however, does not catch calls to Swing components that to not cause a repaint, which might lead to equally embarrassing bugs. An elegant solution to that is the use of aspects to do the detection. I will go more in depth with that solution and aspects in my next posting.
I hope this way too long posting has helped to give you a better understanding of the challenges involved in writing Swing applications and some pointers to a few solutions. In my opinion, if the development team keeps an eye on the few problems outlined here, developing Swing applications can give great results and be very cost efficient. Especially, if developing for several platforms, one of the main design goals of Swing is after all portability, which it has been largely successful.
3 comments:
Nice dude..
I spent many frustrating hours to figure out hoe these gddam threads work.
Good article.
Some more examples would be welcome, especially for the SwingWorker class before < Jdk6
Thank you. Yes more examples are always nice, but it all takes time and work has gone all in as of late. The Swing Worker has been around for a while, you could download it since 1.3 I think, although it was first part of the standard libraries as of Java 6. So before Java 6, I would still use the SwingWorker in pretty much the same way.
Post a Comment