|
This chapter describes the coding conventions that ensure thread-safety for LCDUI, the user interface of the MIDP Reference Implementation. The LCDUI classes are in the javax.microedition.lcdui package. Thread-safety means that access to shared data is safe (data never becomes corrupted, even in the presence of concurrent access) and that the system is live (threads do not deadlock). It is best to understand this chapter before beginning a port of the graphical user interface.
This chapter contains the sections:
All ports of the MIDP Reference Implementation must obey the following thread-safety requirements:
javax.microedition.lcdui.Canvas. (See Chapter 3, "The Event Model” for more information on the events delivered by LCDUI.)
The MIDP Reference Implementation treats all LCDUI data (static variables of all classes and instance variables of all objects) as if it were a single object. It is shared data because this data is accessible to multiple threads. A single, global lock object LCDUILock is defined to protect concurrent access to all shared data.
Because all LCDUI data is treated as a single, shared object, there should not be multiple threads running simultaneously inside of LCDUI. To serialize, the general approach of the MIDP Reference Implementation is to apply locking around the perimeter of LCDUI. All methods that are called from the outside are responsible for ensuring that locking is done properly before making calls into other parts of LCDUI. Method calls into LCDUI enter through the following general routes:
In general, methods internal to LCDUI need not be concerned with locking, and they may assume that their callers have handled locking correctly. In certain cases, internal methods are called without holding the lock, because of complications with the locking protocols. These cases are marked in the source code with a SYNC NOTE comment.
This design implies that LCDUI internal code should not call the same methods that are called from outside, otherwise lock nesting will result. While lock nesting is not a fatal problem, if left uncontrolled it can lead to performance problems. Care has been taken to refactor the code so as to avoid lock nesting. This refactoring has led to an idiom where a method intended to be called from outside (such as a public API method) simply takes the lock and then calls an internal method. Internal LCDUI code calls the internal method directly. By convention, these internal methods are named after their public counterparts, with Impl appended.
An exception to the global lock rule is for the Graphics object, where the API is structured so that painting to the screen is implicitly serialized. Graphics objects for offscreen Image objects do not make any access to LCDUI data, and so they use their own locking convention. See "Graphics Conventions" for a description of this convention.
In order to preserve event serialization semantics (Requirement 2), another lock calloutLock is used when making calls subject to this requirement. In many cases it is unclear whether a particular method call will stay within LCDUI code or will find its way into application code. This is because much of the LCDUI implementation uses the same APIs that are exposed to applications. Thus, the system will hold calloutLock while calling any method that might end up in application code. For cases where the thread stays within LCDUI, taking calloutLock is unnecessary. It might be possible to optimize the system by avoiding taking the calloutLock in such cases, but it’s likely that the cost of testing for this case will largely offset the savings from avoiding the lock.
Deadlock is prevented by establishing a total ordering of all locks in the system. The ordering of locks is as follows, from highest to lowest priority:
calloutLock objectLCDUILock objectThe general rule is that code holding a lock must not acquire any lock that has a higher priority. However, code holding a lock may acquire any lock with lower priority. From LCDUI’s point of view, all application locks have the same priority. LCDUI code must assume that any call into the application will take application locks, and any call coming from the application holds application locks.
This ordering is a consequence of the thread-safety requirements. Application code that is called by LCDUI must be allowed to take any lock, and application code calling into LCDUI must be allowed to hold any lock (Requirement 3). Since holding LCDUILock is required for access to LCDUI data in methods called from the application (Requirement 1), LCDUILock must have a lower priority than all application locks. Since calloutLock must be held in order to serialize calls into the application (Requirement 2), calloutLock must have a higher priority than all application locks.
This section covers thread-safety coding conventions for LCDUI. It has the topics:
The general thread-safety conventions for LCDUI are as follows:
LCDUILock while making any access to LCDUI shared data (class or instance variables)LCDUILock prior to manipulating any LCDUI dataLCDUILockcalloutLock may take LCDUILockLCDUILock must release it before taking calloutLockcalloutLock around this callcalloutLockLCDUILock must not call into the applicationSYNC NOTE comment
Within each call in the public API, there is an appropriate synchronized block. For example, in Gauge.java:
public void setMaxValue(int maxValue) {
synchronized (Display.LCDUILock) {
setMaxValueImpl(maxValue);
}
}
Methods that require the Display.LCDUILock cannot merely use the synchronized method modifier because this would indicate that synchronization is to be performed on the object itself, not on the LCDUILock object. The rule is that LCDUILock must be held whenever there is access to any shared data, that is, data that is potentially accessible to multiple threads. This includes class and instance variables belonging to this class or to any other LCDUI class. Local variables and method parameters are private to each thread and are not shared data. Access to them does not require locking.
Note that the synchronization block begins after argument checking. This is safe, because the argument checking does not involve any access to shared data. This is a tiny optimization. It would also have been correct to have the synchronization block around the entire body of the method.
Certain simple methods may not need synchronization at all. These cases are documented with a explanatory SYNC NOTE comment. For example, in Gauge.java:
In this case, the value being returned is an int, which the Java™ Language Specification requires to be manipulated atomically. (That is, a simultaneous reads and writes to this value will never result in a mixture of old and new bits. If the value were a long, this property wouldn’t hold.) The value returned will either be the old value or the new value; which it will be is unpredictable. Adding locking doesn’t change the situation, and so locking is omitted from these cases for purposes of efficiency. There are a handful of “getter” methods such as this that can avoid synchronization.
Strictly speaking, this code should be synchronized because of memory ordering issues. In practice, memory ordering issues arise only on multiprocessor systems, and it is very unlikely that this code will be executed on such systems. The MIDP Reference Implementation, therefore, made a design decision to avoid locking overhead in cases such as these by relying on the virtual machine (VM) to provide sequentially consistent memory semantics.
Locking must occur around assignment of atomic values. Consider the following hypothetical example:
public void setIndex(int newIndex) {
synchronized (lock) {
index = newIndex; // index is an instance variable
}
}
One might be tempted to omit this locking, because the assignment is atomic, and locking would not seem to add anything. However, holding the lock is necessary to ensure that if another thread reads this variable several times while holding the lock, these reads will return consistent values.
Constructors typically do not need any synchronization if all they do is initialize the newly created object. However, individual cases will need to be inspected carefully in order to determine whether they might affect other data structures. For example, in Form.java:
public Form(...) {
super(title); // (a)
synchronized (Display.LCDUILock) { // (b)
// long, complex initialization
}
}
At (a), the call to super ends up creating a new Layout object. Without tracing the entire call tree, it is difficult to determine whether the call to the superclass constructor has any access to shared data. It is therefore reasonable to want to put locking around this call. Unfortunately, a constructor’s call to super must appear as its very first statement and cannot appear within a synchronized block. Therefore, the superclass constructor must be inspected carefully to ensure that there is no unsynchronized access to shared data.
The synchronization block beginning at (b) surrounds a complex initialization routine. Once again, without inspecting the entire call tree, it is difficult to tell whether this routine makes any access to shared data. Thus code like this has been placed within a synchronization block so that access to shared data is safe. Even if the code in its current state makes no access to shared data, a future modification to the code might add such an access. Leaving this code unlocked is therefore quite fragile.
Even if the body of this constructor and the body of the superclass constructor are synchronized, there is still a potential safety problem. Suppose the superclass constructor were to store a reference to the object being constructed into a shared data structure. There is a window of time between the release of the lock held by the superclass constructor and the reacquisition of the lock by the body of this constructor. During this window, other threads will have access to a reference to this object. If another thread were to call a method on this object, that method would be operating on the object in a partially constructed state, which might lead to misbehavior or errors. Therefore, constructors must be extremely careful not to store references to the newly constructed object into shared data, even if the store is done within a synchronization block.
Event handling methods and MIDlet state change methods are considered to originate outside LCDUI and thus must also hold LCDUILock during access to shared data. The EventHandler thread makes calls on the DisplayAccessor object in order to deliver events to LCDUI. From the DisplayAccessor object, the call tree fans out to individual Displayable objects. Every method of Displayable that is called from within the DisplayAccessor must be inspected in order to determine whether it should be locked. In practice, this means that LCDUI classes that are subclasses of Displayable must add synchronization within their showNotify, hideNotify, keyPressed, paint, and other event delivery methods. Note that subclasses of the Item class have these methods but that they are not locked. This is because the Form object receives the event call, takes the lock, and then calls the corresponding method on the appropriate Item object.
In some cases, the processing of an event will end up calling out to the application. (These are callbacks from the point of view of the application, but this section takes the point of view of the system and calls them callouts.) The LCDUILock must not be held during any callout, because doing so may give rise to deadlock. However, if the current Displayable is not a Canvas object, then it will be a high-level UI component that is part of LCDUI. Each component will be responsible for reacquiring the LCDUILock for itself as described above.
In order to serialize callouts (Requirement 2), the code must hold calloutLock across any call that might end up in the application. These calls include the following:
Canvas.hideNotifyCanvas.keyPressedCanvas.keyRepeatedCanvas.keyReleasedCanvas.paintCanvas.pointerDraggedCanvas.pointerPressedCanvas.pointerReleasedCanvas.showNotifyCanvas.sizeChangedCommandListener.commandActionCustomItem.getMinContentHeightCustomItem.getMinContentWidthCustomItem.getPrefContentHeightCustomItem.getPrefContentWidthCustomItem.hideNotifyCustomItem.keyPressedCustomItem.keyRepeatedCustomItem.keyReleasedCustomItem.paintCustomItem.pointerDraggedCustomItem.pointerPressedCustomItem.pointerReleasedCustomItem.showNotifyCustomItem.sizeChangedCustomItem.traverseCustomItem.traverseOutDisplayable.sizeChangedItemCommandListener.commandActionItemStateListener.itemStateChangedRunnable.run resulting from a call to Display.callSerially
Because calloutLock has a higher priority than LCDUILock, the code must release LCDUILock prior to taking calloutLock and calling into the application. An example of this occurs in CustomItem.java where the application’s ItemStateListener is called:
void callKeyPressed(int keyCode) {
ItemCommandListener cl = null;
Command defaultCmd = null;
synchronized (Display.LCDUILock) {
cl = commandListener;
defaultCmd = defaultCommand;
} // synchronized
// SYNC NOTE: The call to the listener must occur outside
// of the lock
try {
// SYNC NOTE: We lock on calloutLock around any calls
// into application code
synchronized (Display.calloutLock) {
if ((cl != null) ... ) {
cl.commandAction(defaultCmd, this);
} else {
this.keyPressed(keyCode);
}
}
} catch (Throwable thr) {
Display.handleThrowable(thr);
}
}
(Some error checking has been omitted for brevity.)
This code uses the open call technique of loading information into local variables while the lock is held, and ensuring that unlocked code uses only these local variables. This is necessary in case the value of the itemStateListener instance variable or the items array is changed between the set and the actual call.
A consequence of this structure is that the call to the application must occur at the same level in the calling structure as the locking of LCDUILock, because LCDUILock must be released before calloutLock is taken and the call made into the application. This in turn implies that the information about the decision of whether to make the itemStateChanged call must be returned from the Item that made the decision. The call cannot be made directly from the code in the Item that handles the event.
Since the calls to a Canvas object’s paint method are serialized, the Graphics object passed to it need not be synchronized at all. This relies on the underlying graphics library to be thread-safe. It also assumes that native methods implicitly provide exclusion, as is the case in KVM.
However, a Graphics object whose destination is an Image will need to be synchronized, since multiple threads may attempt to use the Graphics object to paint simultaneously. The locking occurs on the Graphics object itself, not on LCDUILock, because the effect of any graphics call affects only the state of that Graphics object. This locking is applied in ImageGraphics, a private implementation subclass of Graphics, leaving the main Graphics class without locking:
class Graphics {
public void clipRect(int x, int y, int width, int height) {
...
}
...
class ImageGraphics extends Graphics {
public void clipRect(int x, int y, int w, int h) {
synchronized (this) {
super.clipRect(x, y, w, h);
}
}
...
}
Locking is necessary for certain methods, such as clipRect, because it manipulates the Graphics object’s state using code written in the Java programming language. However, the drawLine method is simply a direct call to a native method, which the MIDP Reference Implementation assumes runs single-threaded, and so no locking is necessary. The subclass can add no value by taking a lock around the native call, so it simply inherits the native implementation.
Note that it is not necessary to lock the destination image of any graphics call nor the source image of the drawImage call. The assumption is that the only operations that have access to actual image data are native, and so no exclusion is necessary. Graphics operations from different threads might be interleaved, but locking within the Graphics class will not prevent that from occurring; that is the application’s responsibility.
The MIDP 2.0 Specification requires that, in general, callouts be serialized. That is, a callout may not begin until the previous callout has returned. (This rule is covered by Requirement 2.) However, the specification for serviceRepaints states that it must not return until the paint method is called and has returned. This is true even if serviceRepaints is called from within an application callout. This is the only case where two callouts are allowed to be in progress simultaneously.
This arrangement gives rise to the possibility of deadlock. Suppose that the event thread has taken calloutLock and is calling an application’s event method, which attempts to acquire one of the application’s locks. Suppose further that an application thread already holds this application lock and now calls serviceRepaints in order to force a pending repaint to be processed. The serviceRepaints code attempts to acquire calloutLock in order to call paint, and the system is now deadlocked.
The MIDP 2.0 Specification prevents deadlock in this case by imposing a special rule, which is that applications are prohibited from holding any of their own locks during a call to serviceRepaints. This rule is reflected in the lock ordering policy described in "Design Approach" . Since serviceRepaints may call paint, which requires holding calloutLock, the rule about holding no locks still applies. This is the only case where the lock ordering rules are exposed to the application. The application is allowed to hold its locks during a call to any other LCDUI method, in accordance with Requirement 3. The serviceRepaints method is the sole exception to this rule.
A multi-threaded application will generally need to hold locks on its own data structures in order to protect them from concurrent access. The problem is that the application is prohibited from holding these locks when it calls serviceRepaints. This makes serviceRepaints hard to use correctly. In practice, application code must use the open call technique in order to use serviceRepaints safely.
|
Porting MIDP MIDP Reference Implementation, Version 2.0 FCS |
Copyright © 2002 Sun Microsystems, Inc. All rights reserved.