C H A P T E R  2

Multitasking Safety

The Java Wireless Client software provides the ability to run multiple MIDlets concurrently in a single OS process. From the standpoint of the OS, there is one process and one Java virtual machine. However, from the standpoint of a Java application, it appears as if it is running in its own, independent virtual machine, isolated from other Java applications.

These apparently independent virtual machines are called tasks. Each MIDlet runs in its own task. When a new MIDlet is started, a new task is created for it. When a MIDlet exits, its task is destroyed. The Application Management Software (AMS) runs in a dedicated task, and it is running the entire time the Java Wireless Client software is active.

Although tasks cannot interact (they cannot access each other's objects, for example), they do share native code, process resources, and external resources. This sharing leads to some implementation issues, both in native code and in Java programming language code (Java code). The Java Wireless Client software takes these issues into account. Those who want to integrate existing libraries, which might not have been written with multitasking in mind, also need to be aware of the issues so that they can add the source code correctly.

Code integrated into the Java Wireless Client software must be multitask safe. That is, it must maintain the independence of one task from another. Because tasks run in a single operating system process, the OS process can run native code for any task. To keep tasks from interfering with each other and with each other's data, the code must be made aware of the task on whose behalf it is being called and possibly allocating resources. When the task context is established, the code is considered multitask safe.

This chapter points out issues that might arise in ensuring native code is multitask safe. It also provides techniques for making the code multitasking safe in different situations.

The following list summarizes the multitasking safety issues to consider when you update or add native code for your port:


Multitask Safety and Multithread Safety

Many systems today are multithreaded, which requires code that runs in these systems to be thread safe or multithread safe. For example, POSIX Threads (Pthreads) enables multiple native threads to run in the same OS process. Each native thread has access to native memory, so any data structures in native memory must be protected from concurrent access. This is typically accomplished through the use of locks to provide mutual exclusion.

Similarly, multiple Java platform threads can run in the same Java virtual machine. Each Java platform thread has access to the objects in the Java virtual machine, so these objects must also be protected from concurrent access. Java code typically accomplishes this through the use of synchronized code blocks or higher-level constructs.

In the Java Wireless Client software, each task has one or more threads. Each of these threads has concurrent access to the objects in that task, and so the same multithread safety issues occur in Java Wireless Client software as in conventional Java virtual machines. However, a thread in one task has access only to objects within its task, and it has no access to objects in any other task. The multitasking nature of Java Wireless Client software thus has no impact on the multithreaded safety of applications.

All of the tasks in Java Wireless Client software run in a single OS process, and therefore they all have access to the same native memory and data structures. Threads from different tasks are scheduled arbitrarily. Therefore, native methods that read or update native data structures must be prepared to deal with operations from different tasks being interleaved in an arbitrary order. Furthermore, because different tasks are generally running different applications, these different applications are likely to place quite different demands on the underlying native system.

For example, certain native functions (such as file storage) must be maintained on a per-application basis. In a single-tasking system, only one application is running, and so all file access is on behalf of that one application. In a multitasking system, several applications are running, and so the file access code can no longer assume that just one application exists. Instead, it must be aware of the possibility of multiple applications, so that one application doesn't accidentally operate on another application's files. Code that is aware of this possibility is multitask safe.

While operations from different tasks can occur in an arbitrary order, no possibility exists of actual concurrency among native methods. One native thread exists in the Java Wireless Client software. When it is running a native method, no possibility exists of a context switch that causes another native method to be run at the same time. Each native method runs to completion and returns before the next native method can begin. For this reason, every native method is a critical section, and it is usually not necessary to perform any OS-level locking (such as with Pthreads mutexes) in native methods. It is only necessary to use OS-level locking if other native threads are active in the system while the Java Wireless Client software is running.

Thus, while native code that runs within Java Wireless Client software must be multitask safe, it need not be multithread safe.


Global and Static Data

In a single-tasking system, it is common for native code to use global or static variables. This works because only one application is running at a time. Global data is implicitly associated with the currently running application. In a multi-tasking system, the multiple applications likely conflict over global data. Therefore, to make your code multitask safe, you might need to rearrange your native data to be allocated on a per-task basis instead of globally.

To do this, you must allocate native data dynamically instead of statically. In addition, you need to associate each piece of native data with a particular task.

To manage native data on a per-task basis, store a pointer to the native data into an int field in a Java object. Associating the native resource with a single Java object implicitly provides multitasking safety because each Java object resides in exactly one task. It is also a good object-oriented approach.

Create a class to represent the native resource. Give the class a private field to hold the pointer to the resource, for example:

private int nativePtr;

Maintain the following invariants:

In native code, when you allocate memory, use KNI field access to store the pointer in the private field. When you free the native memory, use KNI field access to store 0 in the field. Using the K Native Interface (KNI) field access avoids race conditions.

Have operations that use the native pointer use KNI field access for consistency. Have those operations check for a NULL value before they use the pointer, and throw an appropriate exception if the field does not have a pointer. An appropriate exception is a NullPointerException.


Singletons

CLDC HotSpot Implementation 2.0 isolates the logical virtual machines, but there is one situation to consider if you add or change Java code: Singletons. Singleton classes are often used when only one instance of a class should exist. Sometimes the class itself is used as a singleton, and no instance of it is ever created. The way to handle singleton depends on whether you mean it to be used on a per-application (that is, per-task) basis or whether you mean it to be used by the entire system.

For example, in Java Wireless Client software, an event queue is a per-task singleton, because each task has its own event queue. Per-task singletons are not a problem as they are handled by the VM. Static state is automatically replicated on a per-task basis. On the other hand, the singleton that holds the foreground display is a system-global singleton. Only one Display can be in the foreground in the entire system. All other display instances must be in the background. System-global singletons require additional work.

To handle a system-global singleton, consider either maintaining the singleton's state in a single task (such as the AMS task) and communicate the updates through messages, or migrating key pieces of information into native memory, with access to them arbitrated through native methods.

If you maintain the singleton's state in a single task and update it using events, be aware that events are asynchronous messages. Organize the updates so that operations are tolerant of being executed out of order. Although messages are generally processed in the order received, messages from different tasks might not be processed in the order they are sent. Also organize the updates so that requestors can proceed asynchronously. Asynchronous messages do not have any acknowledgement by default, so the sender cannot know when the message is processed or whether it is processed successfully.

You might find that you cannot organize the singleton's maintenance in this way, because its state must be updated synchronously and atomically. Maintaining the foreground state is an example of this type of singleton. In this case, migrate a key piece of state into native memory and handle updates through calls to native methods.


Multitasking Safety Example

Consider a simple though somewhat contrived example of controlling an external device, a microwave oven. Assume that this device has a native API defined in the header file shown in CODE EXAMPLE 2-1.

CODE EXAMPLE 2-1 Native API for a Microwave Oven
/* mw.h */
 
/*
 * Status codes passed to cook callback. The cooking might have been
 * interrupted, for instance, if the user pressed the STOP button or
 * opened the oven door.
 */
typedef enum {
    MW_DONE,         /* cooking finished normally */
    MW_INTERRUPTED,  /* cooking interrupted */
} MWSTATUS;
 
/* Callback for the cook operation. */
typedef void (*MWCB)(MWSTATUS status, void *context);
 
/* Initializes the microwave oven. Must be called exactly once. */
extern void mw_init(void);
 
/* Sets the cook time for the next cook operation, in seconds. */
extern void mw_settime(int nsec);
 
/*
 * Sets the cook power for the next cook operation, an integer
 * in the range [1-100].
 */
extern void mw_setpower(int power);
 
/*
 * Initiates the cooking operation. When the cooking finishes,
 * the callback is called with the status code. The context pointer
 * is passed to the callback for its own use, unmodified by the
 * library.
 */
extern void mw_cook(MWCB callback, void *context);

Typical usage of this API is shown in CODE EXAMPLE 2-2.

CODE EXAMPLE 2-2 Typical usage of the microwave
void cb_popcorn(MWSTATUS status, void *context) {
    if (status == MW_INTERRUPTED) {
        /* tell the user that the popcorn isn't finished */
    } else {
        ...
    }
}
 
void cook_popcorn() {
    mw_init();
    mw_settime(180);
    mw_setpower(100);
    mw_cook(cb_popcorn, NULL);
}

For the sake of discussion, assume that this native library is extremely simplistic. If the mw_init() function is called twice, the system breaks. Or, if mw_cook() is called while the microwave is cooking, the system breaks.

A straightforward binding of Java programming language APIs (Java APIs) for the microwave library might be as shown in CODE EXAMPLE 2-3.

CODE EXAMPLE 2-3 Simple Java API for the Microwave Oven
package javax.microwave.oven;
 
public class Microwave {
    public static native void init();
    public static native void setTime(int nsecs);
    public static native void setPower(int power);
    public static native int cook();
    private Microwave() { } /* prevent instance creation */
}

The implementation of these native methods is straightforward and is not shown here. See the K Native Interface (KNI) Specification, Version 1.0 for further information about writing native methods.

To make this a nice Java API, assume that instead of being callback-based, the cook() method blocks the calling thread until the operation completes or is interrupted. This can be accomplished using SNI_BlockThread, and the native callback can set a flag to cause JVMSPI_CheckEvents to call SNI_UnblockThread. See the CLDC HotSpot Implementation Porting Guide for more information.

Multithread Safety

The most obvious problem with the interface defined in CODE EXAMPLE 2-3 is that it is not thread-safe. A single Java platform thread (Java thread) calling the APIs can certainly use it effectively, but if another thread attempts to use the API, things almost certainly break. For example, one thread might call setTime(). Another thread might be scheduled and issue a call to setTime() with a different value. When the first thread calls cook(), the cook time used is actually the cook time set by the second thread. When the second thread calls cook(), the cooking operation initiated by the first thread might still be going on, which gives rise to an error.

A reasonable way to make this interface thread safe is to introduce mutual exclusion, so that one thread can be assured that no other threads are interfering with its operation. This ensures that intermediate state (such as cook time) is not altered between the setup calls and the cook() call. It also ensures that only one cook() operation can be processed at once, a restriction imposed by the underlying native API.

Because exclusion is required around multiple calls to this interface, making the methods synchronized is insufficient. Therefore, introduce a lock() and unlock() protocol that clients are required to use around their calls to other methods. Each native method is wrapped with a Java method that checks for the proper lock state before proceeding to call the native method.

CODE EXAMPLE 2-4 Introducing a Locking Mechanism for Thread Safety
public class Microwave {
    private static boolean initialized = false;
    private static Thread owner = null;
 
    public static synchronized void lock()
            throws InterruptedException {
        while (owner != null) {
            wait();
        }
 
        owner = Thread.currentThread();
 
        if (!initialized) {
            init();
            initialized = true;
        }
    }
 
    public static synchronized unlock() {
        owner = null;
        notifyAll();
    }
 
    public static synchronized setPower(int power) {
        if (owner != Thread.currentThread()) {
            throw new IllegalStateException();
        }
        n_setPower(power);
    }
 
    public static synchronized setTime(int nsecs) {
        if (owner != Thread.currentThread()) {
            throw new IllegalStateException();
        }
        n_setTime(nsecs);
    }
 
    public static synchronized int cook() {
        if (owner != Thread.currentThread()) {
            throw new IllegalStateException();
        }
        return n_cook();
    }
 
    private static native void init();
    private static native void n_setTime(int nsecs);
    private static native void n_setPower(int power);
    private static native int n_cook();
    private Microwave() { } /* prevent instance creation */
}

Expected usage of this new API from Java platform programs is shown in CODE EXAMPLE 2-5.

CODE EXAMPLE 2-5 Using the Locking Mechanism
    Microwave.lock();
    try {
        Microwave.setTime(180);
        Microwave.setPower(100);
        if (Microwave.cook() == ...) {
            ...
        } else {
            ...
        }
    } finally {
        Microwave.unlock();
    }

This API is now multithread safe. However, it is not multitask safe. The reason is that the thread safety properties are achieved using mechanisms that belong to the Microwave class. These include static variables (initialized, owner) of the Microwave class. Thread synchronization and wait and notify operations use the monitor lock owned by the class. All of these mechanisms are visible to the threads of a single task, and thus they provide safety among the multiple threads of a single task.

However, in a multitasking environment, the class static variables and monitor lock are replicated in each task. Thus, if a thread in task A is performing an operation on the Microwave class, the initialized variable is true and the owner variable contains a reference to the thread performing the operation. However, in task B, the initialized variable still has the value false and the owner variable is null. If a thread in task B attempts an operation, it takes the lock (even though a thread in task A "owns" the lock in task A), it calls init() for the first time (from its point of view). This results in a second call to the native mw_init() function, which is an error. The thread in task B then calls some of the setup functions and then calls cook(), possibly before the cook operation initiated from task A completes, which is another error.

Multitask Safety

Use the microwave library and the Java programming language interface to illustrate how a library must be changed to be multitask safe.

The first and simplest case is class data (static variables) that is semantically global but is replicated into each task. Sometimes the singleton pattern is used in this fashion. Inspect each singleton Java class to determine whether it needs to be a singleton only from the point of view of Java threads that have access to it, or whether it needs to be a system-wide singleton. In the former case, ordinary singleton classes are sufficient. Even though they are replicated within each task, they can be per-task singletons because each Java thread sees only one instance of the singleton class. In the latter case, for global singletons, the singleton characteristic must apply to the entire system. The typical way to accomplish this is to migrate the relevant data from Java code into native code.

In our example, the global singleton data is a static boolean variable initialized that indicates whether the microwave library has been initialized. This can simply be migrated to an int variable in native code. Once this variable is in native code, it is visible to all tasks using native methods. You can write native methods to set and get this value and take action as appropriate. This technique is illustrated in CODE EXAMPLE 2-6.

CODE EXAMPLE 2-6 Migrating the Initialization Variable to Native Code (Doesn't Work)
// Microwave.java
 
    static native boolean getInitState();
    static native void setInitState(boolean init);
 
    public static synchronized void lock()
            throws InterruptedException {
        ...
        if (!getInitState()) {
 
            init();
            setInitState(true);
        }
    }

Unfortunately, the code in CODE EXAMPLE 2-6 does not work. The reason is that threads from different tasks can be switched at any time. Suppose that a thread in task A calls getInitState(), which returns false. The thread decides that the microwave library needs to be initialized, and it starts to execute the code in the if block. Now suppose the system switches to run a thread in task B that is also about to run this code. Task B's thread also calls getInitState(), which also returns false, and so this thread also decides to execute the code in the if block. This results in two calls to the native mw_init() function, which is an error.

From the Java programming language point of view, the tasks are completely isolated from each other, and therefore threads from different tasks cannot interact. This means that there is no Java programming language construct that can provide mutual exclusion between threads that are in different tasks. Therefore, some other kind of mechanism at the native method level is necessary, because native methods operate outside the constraints of the Java programming language.

Instead of the flag test and set logic being in Java code, it must also be migrated to native code, along with the flag itself. In this case, the Java code can call init() every time and it can rely on the logic in native code to ensure that the mw_init() function is called only once, as shown in CODE EXAMPLE 2-7.

CODE EXAMPLE 2-7 Migrating Initialization to Native Code
/* microwave.c */
 
static int initialized = 0;
 
KNIEXPORT KNI_RETURNTYPE_VOID
Java_javax_microwave_oven_Microwave_init(void)
{
    if (initialized == 0) {
        initialized = 1;
        mw_init();
    }
    KNI_ReturnVoid();
}

Note that no mutual exclusion is necessary in native methods. The CLDC HotSpot Implementation has a single thread that runs all Java threads and all native methods. This thread can run at most one native method at a time. The system cannot context-switch to another Java thread while it is in the midst of a native method. Therefore, in the CLDC HotSpot Implementation, all native methods are essentially critical sections. This makes it possible to write native code without many concurrency considerations.



Note - Your system might employ multiple native threads. If this is the case, you might need to employ native-level mutual exclusion facilities such as Pthreads mutexes.



While it is convenient to treat each native method as a critical section, one must ensure that data access and updates are done within a single native method. If any logic about updating native state is performed by Java code, such as mixing Java and native methods, or by making multiple native method calls, this creates race conditions. Running any Java code provides an opportunity for Java threads to be context switched, and conditions established by code before the context switch might be invalid after the context switch. The example from CODE EXAMPLE 2-6 suffers from exactly this problem.

Establishing Per-Task Context

Migrating Java platform data to native can work well for global singletons, but it doesn't work for other situations. In the case of the cook operation, the data is built using a sequence of setup calls to mw_settime() and mw_setpower(), followed by a call to mw_cook() that initiates the operation using the parameters previously set up. The library implicitly stores this information in its internal static data. Thus, it's effectively global.

But no global data exists because the data really belongs to the thread that's setting up to initiate the cooking operation. In this case, the data needs to be migrated upward to be closer to the calling thread. The setup data doesn't need to be sent to the microwave library until immediately prior to the call to the mw_cook() function.

The lock() and unlock() static methods were added to the Microwave class to protect the context that was being built implicitly in library static data by the setTime() and setPower() methods. This locking protocol effectively provides mutual exclusion around library static data. This works in a single-tasking environment, but it fails in a multitasking environment, as described earlier.

If the setup methods simply set values into Java variables, and these values are set into the library immediately before the call to mw_cook(), a locking protocol that makes the setup calls and the cook call atomic is no longer necessary. This technique doesn't work in a multitasking environment anyway. Therefore, it must be removed. The setup context logically belongs to the calling thread. Multiple threads could lock, wait, and notify over static data, but this too is no longer necessary if each thread is permitted to operate on its own instance of a Microwave object.

After making these changes, the resulting Java code is shown in CODE EXAMPLE 2-8.

CODE EXAMPLE 2-8 Keeping State in Java Code
public class Microwave {
 
    int nsecs;
    int power;
    private int nativePtr;
 
    public void setTime(int nsecs) {
        // error checking elided for brevity
        this.nsecs = nsecs;
    }
 
    public void setPower(int power) {
        // error checking elided for brevity
        this.power = power;
    }
 
    public int cook() {
        init();
        return n_cook();
    }
 
    private native void init();
    private native void n_cook();
    public Microwave() { } /* allow instance creation */
    private native void finalize();
}

The responsibility of the n_cook() native method is now much greater. It must block the calling thread until any other cooking operation has completed. It must retrieve the field values from the object and set them into the library. Finally, it must block until this cooking operation has completed. Note that to avoid race conditions, all of this must be performed by a single native method.

For the sake of simplicity, ignore the situation where another cooking operation might already be in progress, and ignore the logic for block and unblocking the calling thread.

Note also the use of a technique for allocating a native context object and storing its pointer in a Java object field. All data used in this native method is relative to the Java object on which it's called. This automatically provides multitask safety. An object belongs exclusively to a single task, so operations that are interleaved or that occur simultaneously in different tasks cannot interfere with each other.

CODE EXAMPLE 2-9 Implementing the Native n_cook() Method
/* microwave.c */
 
KNIEXPORT KNI_RETURNTYPE_INT
Java_javax_microwave_oven_Microwave_n_cook(void)
{
    int nsecs;
    int power;
    MWSTATUS *statusp;
    int retval;
 
    jfieldID nsecsFieldID;
    jfieldID powerFieldID;
    jfieldID nativePtrFieldID;
 
    KNI_StartHandles(2);
    KNI_DeclareHandle(thisObj);
    KNI_DeclareHandle(microwaveClass);
 
    KNI_GetThisPointer(thisObj);
    KNI_GetObjectClass(thisObj, microwaveClass);
 
    nsecsFieldID = KNI_GetFieldID(microwaveClass, "nsecs", "I");
    powerFieldID = KNI_GetFieldID(microwaveClass, "power", "I");
    nativePtrFieldID = 
        KNI_GetFieldID(microwaveClass, "nativePtr", "I");
 
    if (/* this is the first invocation */) {
        statusp = (MWSTATUS *)malloc(sizeof(MWSTATUS));
 
        nsecs = KNI_GetIntField(thisObj, nsecsFieldID);
        power = KNI_GetIntField(thisObj, powerFieldID);
        KNI_SetIntField(thisObj, nativePtrFieldID, (jint)statusp);
 
        mw_settime(nsecs);
        mw_setpower(power);
        mw_cook(callback, statusp);
        SNI_BlockThread();
        retval = -1; /* return value ignored if caller is blocked */
    } else {
        /* this is a reinvocation after having been awakened */
        statusp = 
            (MWSTATUS *)KNI_GetIntField(thisObj, nativePtrFieldID);
        retval = (int)(*statusp);
        KNI_SetIntField(thisObj, nativePtrFieldID, 0);
        free(statusp);
    }
 
    KNI_EndHandles();
    KNI_ReturnInt(retval);
}
 
 
void
callback(MWstatus status, void *context)
{
    MWSTATUS *statusp = (MWSTATUS *)context;
    *statusp = status;
 
    /*
     * Search the list of block threads and find the right
     * one to unblock with SNI_UnblockThread(). See the CLDC HI
     * Porting Guide for further information.
     */
}
 
 
KNIEXPORT KNI_RETURNTYPE_INT
Java_javax_microwave_oven_Microwave_finalize(void)
{
    jfieldID nativePtrFieldID;
    MWSTATUS *statusp;
 
    KNI_StartHandles(2);
    KNI_DeclareHandle(thisObj);
    KNI_DeclareHandle(microwaveClass);
 
    KNI_GetThisPointer(thisObj);
    KNI_GetObjectClass(thisObj, microwaveClass);
    nativePtrFieldID = 
        KNI_GetFieldID(microwaveClass, "nativePtr", "I");
    statusp = (MWSTATUS *)KNI_GetIntField(thisObj, nativePtrFieldID);
 
    if (statusp != NULL) {
        free(statusp);
        KNI_SetIntField(thisObj, nativePtrFieldID, 0);
    }
}

These examples show how multitask safety can be achieved by judicious migration of data from Java code into native code (for global singletons) and from native code into Java code (for context-specific data). Some libraries have explicit context objects, with all operations relative to that context object.

For cases like that, a reliable technique is to put the native context pointer into a nativePtr int field in the Java object. In other cases, such as the microwave library, the library maintains implicit context in its own static data. For these cases, it is necessary for the code to create its own context. This context can be stored as fields in the Java object itself, or a data structure can be allocated on the native heap and a pointer to this structure placed into a nativePtr int field. Ensuring that the context for all operations is relative to a Java object automatically provides multitask safety.