JDK 1.1 for Solaris Developer's Guide

Chapter 2 Multithreading

This chapter discusses multithreading in general, and specific MT issues related to Java on Solaris software and the native-threaded JVM.

Information for developers new to Java is starred(*).

Definition of Multithreading*

A thread is a sequence of control within a process. A single-threaded process follows a single sequence of control while executing. An MT process has several sequences of control, thus is capable of several independent actions at the same time. When multiple processors are available, those concurrent but independent actions can take place in parallel.

Java Threads in the Solaris Environment -- Earlier Releases*

Previous to Java on Solaris 2.6 software, the Java runtime used a user-level threads library called "green threads," part of the Java runtime thread and system support layer. Because the green threads library was user-level and the Solaris system could process only one green thread at a time, Solaris handled the Java runtime as a many-to-one threading implementation (refer to "Many-to-One Model (Green Threads)"). As a result, several problems arose:

To substantially increase application performance, the green threads library was replaced with native Solaris threads for Java on the Solaris 2.6 platform; this is carried forward on the Solaris 7 and Solaris 8 platforms.

Multithreading Concepts*

MT programming enables you to speed up applications and to leverage the parallelism of hardware and the efficiencies of objects. The Java on Solaris MT implementation is efficient, reliable, and standards-based, offering significant advantages to developers and end-users. The Solaris operating environment provides the best performance, tools, support, and flexibility in developing MT applications. The Solaris operating environment utilizes the following significant MT advances:

Benefits of Multithreading*

This concurrent activity speeds applications up - one of the main benefits of multithreading.

MT allows both the full exploitation of parallel hardware and the effective use of multiple processor subsystems. While MT is essential for taking advantage of the performance of symmetric multiprocessors, it also provides performance benefits on uniprocessor systems by improving the overlap of operations such as computation and I/O.

Some of the most important benefits of MT are:

Multithreading Models

Most multithreading models fall into one of the following categories of threading implementation:

Many-to-One Model (Green Threads)

Implementations of the many-to-one model (many user threads to one kernel thread) allow the application to create any number of threads that can execute concurrently. In a many-to-one (user-level threads) implementation, all threads activity is restricted to user space. Additionally, only one thread at a time can access the kernel, so only one schedulable entity is known to the operating system. As a result, this multithreading model provides limited concurrency and does not exploit multiprocessors. The initial implementation of Java threads on the Solaris system was many-to-one, as shown in the following figure.

Figure 2-1 Many-to-One Multithreading Model


One-to-One Model

The one-to-one model (one user thread to one kernel thread) is among the earliest implementations of true multithreading. In this implementation, each user-level thread created by the application is known to the kernel, and all threads can access the kernel at the same time. The main problem with this model is that it places a restriction on you to be careful and frugal with threads, as each additional thread adds more "weight" to the process. Consequently, many implementations of this model, such as Windows NT and the OS/2 threads package, limit the number of threads supported on the system.

Figure 2-2 One-to-One Multithreading Model


Many-to-Many Model (Java on Solaris--Native Threads)

The many-to-many model (many user-level threads to many kernel-level threads) avoids many of the limitations of the one-to-one model, while extending multithreading capabilities even further. The many-to-many model, also called the two-level model, minimizes programming effort while reducing the cost and weight of each thread.

In the many-to-many model, a program can have as many threads as are appropriate without making the process too heavy or burdensome. In this model, a user-level threads library provides sophisticated scheduling of user-level threads above kernel threads. The kernel needs to manage only the threads that are currently active. A many-to-many implementation at the user level reduces programming effort as it lifts restrictions on the number of threads that can be effectively used in an application.

A many-to-many multithreading implementation thus provides a standard interface, a simpler programming model, and optimal performance for each process. The Java on Solaris operating environment is the first many-to-many commercial implementation of Java on an MT operating system.

Figure 2-3 Many-to-Many Multithreading Model


Multithreading Kernel

The MT kernel is a critical foundation of a complete multithreading implementation. In an MT kernel such as the one used by the Solaris operating environment, each kernel thread is a single flow of control within the kernel's address space. The kernel threads are fully preemptive and can be scheduled by any of the available scheduling classes in the system, including the real-time class. All execution entities are built using kernel threads, which represent a fully preemptive, real-time "nucleus" within the kernel.

In addition, kernel threads employ synchronization primitives that support protocols for preventing the blocking that results in the inversion of thread and process priority. This ensures that applications execute as expected. Kernel threads also allow kernel-level tasks such as NFS daemons, pageout daemons, and interrupts to execute asynchronously, thus increasing concurrency and overall throughput.

The MT kernel is essential to building an MT application architecture, such as a typical JVM implementation:

Advantages of Java Multithreading in the Solaris Environment

The Solaris MT kernel is one of the most important components of the Solaris operating environment, enabling the Solaris system to be the only standard operating environment that provides this level of concurrency, sophistication, and efficiency.

Java on Solaris software leverages the multithreading capabilities of the kernel while also enabling you to create powerful Java applications using thousands of user-level threads for multiprocessor or uniprocessor systems, through a very simple programming interface.

The Java on Solaris environment supports the many-to-many threads model. As illustrated in Figure 2-4, the Solaris two-level architecture separates the programming interface from the implementation by providing an intermediate layer, called lightweight processes (LWPs). LWPs allow you to create fast and cheap threads through a portable application-level interface. To use LWPs, write applications using threads. The runtime environment, as implemented by a threads library, multiplexes and schedules runnable threads onto "execution resources," the LWPs.

Individual LWPs operate like virtual CPUs that execute code or system calls. LWPs are dispatched separately by the kernel, according to scheduling class and priority, so they can perform independent system calls, incur independent page faults, and run in parallel on multiple processors. The threads library implements a user-level scheduler that is separate from the system scheduler. User-level threads are supported in the kernel by the kernel-schedulable LWPs. Many user threads are multiplexed on a pool of kernel LWPs.

Figure 2-4 Solaris Two-level Architecture


Solaris threads provide an application with the option to bind a user-level thread to an LWP, or to keep a user-level thread unbound. Binding a user-level thread to an LWP establishes an exclusive connection between the two. Thread binding is useful to applications that need to maintain strict control over their own concurrency, such as those that require real-time response. No Java API exists to perform the binding. Most Java applications do not require binding. If binding is required, a Solaris native method call can be made to perform the binding.

Therefore, all Java threads are unbound by default. Unbound user-level threads defer control of their concurrency to the threads library, which automatically expands and shrinks the pool of LWPs to meet the demands of the application's unbound threads.

The following unique features of Java on Solaris threads are available by default to all Java applications on Solaris:

Note -

In general, accessing native Solaris features using native methods from a Java application is not recommended. Such usage could make the Java application non-portable, because it would not be 100% Pure JavaTM and would be tied to the Solaris platform only.

Though accessing Solaris-specific features from Java applications is not recommended, here is a list of those features to illustrate the richness of the Solaris MT architecture:

The Solaris two-level model delivers unprecedented high levels of flexibility for meeting many different programming requirements. Certain programs, such as window programs, demand heavy logical parallelism. Other programs, such as matrix multiplication applications, must map their parallel computation onto the actual number of available processors. The two-level model allows the kernel to accommodate the concurrency demands of all program types without blocking or otherwise restricting thread access to system services.

The Java on Solaris design uses system resources efficiently as they are needed. Applications can have thousands of threads with minimal thread-use overhead. Threads execute independently, share process instructions, and share data transparently with the other threads in a process. Threads also share most of the operating system state of a process, can open files and permit other threads to read them, and allow different processes to synchronize with each other in varying degrees.

The Java on Solaris threaded model delivers the best combination of speed, concurrency, functionality, and kernel resource utilization.

Grouping Threads

Every Java thread is a member of a thread group. Thread groups provide a mechanism for collecting multiple threads into a single object and manipulating those threads all at once, rather than individually.

For example, you can start or suspend all the threads within a group with a single method call. Java thread groups are implemented by the ThreadGroup [(in the API reference documentation)] class in the java.lang package. The runtime system puts a thread into a thread group during thread construction. When you create a thread, you can either allow the runtime system to put the new thread in a reasonable default group or you can explicitly set the new thread's group. The thread is a permanent member of whatever thread group it joins upon its creation; you cannot move a thread to a new group after the thread has been created.

Java Threads Issues

This section discusses Java-generic and Solaris-specific issues that might be of concern if you are writing Java applications for the Solaris product.

Generic Java Issues

Numerous methods have been deprecated for JDK 1.1. Refer to Table 4-1 for a complete list.

Solaris-Specific Issues

Some issues are specific to Solaris, as explained in the following sections.

Using Multithreading-Unsafe Libraries

Caution - Caution -

This workaround is not trivial and can cause deadlocks if not carefully programmed. Do this only if absolutely unavoidable.

If you try to run a multithreaded Java application that also uses native C/C++ code with previously-released libraries that have not been compiled with the -D_REENTRANT flag on, you could encounter problems, as explained here.

With a native-threaded JVM such as 1.1, libc stores system call error code in a thread-specific errno. When an mt-unsafe library references errno, it references the global version, because it was not compiled with the -D_REENTRANT flag on. Therefore, the library can't access the thread-specific errno and its errno-dependent response to a failed system call would be incorrect.

The real solution is to ensure that an MT Java application that also uses native code by way of native methods is linked with MT-safe (or at least errno--safe libraries).

However, if you cannot avoid referencing errno-unsafe libraries, the following workaround can help: Enable the main thread to enter the Java application and arrange for all calls to the unsafe library to be routed through the main thread. For example, if a thread makes a JNI call, the JVM can marshal all JNI arguments and put them in a queue serviced by the main thread. The thread can wait for the main thread to issue the call and return the results to it.

It is not necessary for calls made from only the main thread to the unsafe library to go through a lock, since calls to the library are single-threaded through the library; only the main thread ever calls the library. The main thread could issue non-blocking calls, and so forth, to ensure some amount of concurrency. The main thread's errno is global, and the same errno would be referenced by both libc and the MT-unsafe library.

interrupt() Method

The use of this method is generally discouraged; it is not currently specified as particularly useful. The Java Language Specification (JLS) defines it as a way to interrupt a target thread only if and when it calls the wait() method.

However, on the Solaris platform, the semantics have been extended so that it also interrupts the target thread's I/O calls. Do not depend on this extension, as it might be discontinued. Additionally, using the extended, I/O interruption semantics of the interrupt method makes the code non-portable across different JVMs.

Thread Priorities

The thread priorities available to Java threads on a native threaded JVM should be treated as hints to the scheduler, especially if the threads are compute-bound. The number of processors available to a process is dynamic and unpredictable. Therefore, an attempt to use priorities to schedule execution on any multi-tasked, multiprocessor system is not likely to succeed.