Thread-Safe Toolbox Access From MRJ
By Jens Alfke
How We Play Safely With the Toolbox
J ava is pervasively multi-threaded. The Mac OS isnt. Most of it isnt re-entrant, and parts of it are very dependent on global state that needs to remain consistent from one call to the next. This can cause big problems when trying to call the Mac OS directly from Java. This technote describes synchronization techniques that will allow your native or JDirect code to play safely when making OS or Toolbox calls, especially as we move forward to Mac OS X.
How We Play Safely With the Toolbox
As we all know, the basics of the Mac OS and Toolbox (which for simplicity Ill just lump together as the Toolbox here) were designed in the early 1980s for a machine that could run only a single app at a time and a single thread of execution in that app. Things have been improved a bit since that time, but the constraints of that basic design require that the Toolbox is still only single-threaded. Cooperative multitasking between applications means process switching occurs only at well known times (when
This causes problems when trying to run multiple threads in a Mac OS app. Toolbox calls are not re-entrant, which means that while one thread is executing a Toolbox call, no other thread can enter the Toolbox. Furthermore, the pervasive use of global state (like the current
We had to deal with these issues in implementing the AWT for MRJ 2.1, since most of the AWT is written in Java code that calls the Toolbox via JDirect2. We used a pretty standard solution of using critical sections in the code wherever the Toolbox is used, with only one thread able to enter such a critical section at a time.
In Java terms, this is implemented by having a single global object (accessed via a public static variable) serve as a synchronization lock, and putting all critical sections into Java blocks synchronized to that object. This object is known as:
(In other words, its a static final variable called
We on the MRJ team call this LOCKing or using the LOCK, capitalized as shown. (When saying it, you emphasize the first syllable to indicate that its capitalized...)
Why You Should Care
We implemented this so that our own AWT would work correctly. But if other developers are going to write code that uses JDirect -- and many of you are -- and if that code is going to run in apps that also use AWT, then we need to use the same synchronization technique so that your code doesnt step on our code and vice versa. Thus, this technote.
Heres a simple example showing a fully synchronized
This isnt an issue specific to JDirect. If you write native methods (probably using JNI) that call the Toolbox, the same issues can arise. If your native code alters Toolbox state like the current port or the state inside a
Back to top
Paranoia and Reality
The previous section is actually a bit alarmist ... for now. The truth is that it is currently not necessary to LOCK around every single Toolbox call -- in fact, individual calls that dont require or change system state -- like the
Note carefully that I said currently. The current state of the art is due to the current limitations of how you can implement threads on the Mac OS, which has a lot to do with the nanokernel at the heart of the system. Future releases of Mac OS 8 (Blue) will have improvements to the nanokernel that might at some future time allow us to support true pre-emption. Moreover, Mac OS X will be based on the Mach 3 microkernel which supports true pre-emptive threads, which Java will use.
This will requires changes to any native method implementations as well (which is not surprising, since all MacOS code will need to be revised to run with the Carbon APIs in OS X.) In OS X it will become possible for a native method to be pre-empted by another thread, which makes synchronization a lot more important. Youll need to start acquiring/releasing the LOCK within your native methods just as you would in the equivalent Java code. Well be providing more details of what kind of synchronization is necessary, as Mac OS X becomes more of a reality.
Therefore, if you want your JDirect- or JNI-based Toolbox calls to continue to function in future releases of the Mac OS with future Java implementations, you should start LOCKing all your Toolbox calls right away. If you dont have time to do it religiously for everything, you at least need to LOCK groups of Toolbox calls that need an undisturbed global environment.
Back to top
The Perils of LOCKing
Unfortunately, all this synchronization comes with dangers of its own, as anyone whos done much multithreaded programming can tell you. The principal one is the classic problem of deadlock.
A short tutorial on deadlock
Deadlock results when a thread holding a monitor (its inside a
The LOCK object is nothing special in this regard, but the fact that youre going to be synchronizing against it a lot means you have to watch out for deadlocks. Heres a typical scenario that causes a deadlock:
This may look innocuous, but consider a thread that calls foo, gets as far as the
Later, the first thread wakes up and tries to enter the foo methods
Now both threads are blocked, and neither can proceed until the other one releases a monitor. Permanent deadlock.
How to avoid this
One of the textbook solutions for avoiding deadlock is to always acquire monitors in the same order. The reason for the deadlock in the above example is that the first thread acquired first the receiving objects lock and then the Toolbox LOCK, while the second thread acquired them in the opposite order.
The ordering weve used in our own code is that the LOCK is always the last monitor acquired. In Java terms, this means that, while synchronized to the LOCK, you should never synchronize on anything else or call any method that synchronizes on anything else. (In the example, the bar method violates this rule when it calls foo. The best fix is probably to move the foo call out of the synchronized block.)
A specific and very important corollary of this is that you should not call into the AWT while holding the LOCK, since the public AWT methods do a lot of synchronization, as do our private peer classes that they call.
If you feel very smart, you can make certain exceptions to this rule -- you can call synchronized methods from with a LOCKed block as long as youre certain that you never LOCK while synchronized against that object. For instance, you may feel safe calling methods on a Vector object within a LOCKed block, even though most Vector methods are synchronized, because its pretty unlikely that you have other code that synchronizes against that Vector.
This leads to a programming style in which you wrap LOCKed blocks pretty tightly around your Toolbox calls. If you have a method that needs to call the Toolbox, do some other Java stuff, then call the Toolbox again, dont LOCK the entire method. Instead, LOCK the first and second group of Toolbox calls, leaving the stuff in the middle out. Of course, this implies that global state might have changed between the first and second group of calls. If this is unacceptable, youll need to have the second group set the state up again, or figure out how to re-order the code so the two groups can be merged into one with the rest of the Java calls coming before or after.
We had so much fun with deadlocks while developing MRJ that we added some support to the MRJ VM to help us debug them.
First, the debug build of MRJLib includes a deadlock sniffer in its thread scheduler. This will detect the classic deadlock case described above, and will immediately drop into MacsBug with a user-break telling you that a deadlock was detected. You can then use the techniques below to get more info. (The debug build is supplied with the MRJ 2.1 SDK. See its accompanying Read-Me file for installation instructions and more info.)
Even with the regular optimized build of MRJLib, if you have the MRJ
Back to top
Back to top
Thanks to the usual suspects.