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(*).
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.
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:
Java applications could not interoperate with existing MT applications in the Solaris environment.
Java threads could not run in parallel on multiprocessors.
An MT Java application could not harness true OS concurrency for faster applications on either uniprocessors or multiprocessors.
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.
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:
The Solaris MT kernel - the essential component in a complete implementation of an MT architecture.
The two-level threads model - the Solaris system's proven MT implementation that enables processes to use an unlimited number of threads for optimal performance
The POSIX pthreads standard - an implementation of the MT interface defined by the IEEE POSIX 1003.1c specification.
The Java threads API, one of the Java APIs, that is fast becoming a standard interface used to program MT applications
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:
Improved throughput. Many concurrent compute operations and I/O requests within a single process.
Simultaneous and fully symmetric use of multiple processors for computation and I/O
Superior application responsiveness. If a request can be launched on its own thread, applications do not freeze or show the "hourglass". An entire application will not block, or otherwise wait, pending the completion of another request.
Improved server responsiveness. Large or complex requests or slow clients don't block other requests for service. The overall throughput of the server is much greater.
Minimized system resource usage. Threads impose minimal impact on system resources. Threads require less overhead to create, maintain, and manage than a traditional process.
Program structure simplification. Threads can be used to simplify the structure of complex applications, such as server-class and multimedia applications. Simple routines can be written for each activity, making complex programs easier to design and code, and more adaptive to a wide variation in user demands.
Better communication. Thread synchronization functions can be used to provide enhanced process-to-process communication. In addition, sharing large amounts of data through separate threads of execution within the same address space provides extremely high-bandwidth, low-latency communication between separate tasks within an application.
Most multithreading models fall into one of the following categories of threading implementation:
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.
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.
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.
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:
It is fully symmetric in order to maximize multiprocessor performance.
With its multiple kernel threads, it enables parallelism on multiprocessor machines. This improves the efficiency of hardware subsystems by providing concurrency and parallelism in computation, networking, display, and I/O.
Traditional (single-threaded) applications run unchanged on an MT kernel.
It is fully preemptive, providing real-time responsiveness.
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.
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:
Unbound Solaris threads: A Java thread is essentially the same as an unbound Solaris thread, with the inherent advantages of unbound threads.
The ability to share an LWP with several user-level threads
Automatic concurrency control for unbound threads. The threads library dynamically expands and shrinks the pool of LWPs to meet the demands of the application. See "Programming Compute-Bound, Parallellized Java Applications" for more information.
Extremely lightweight user threads that can be created, used, and discarded in very large numbers without consuming excessive system resources or degrading system performance
Synchronization primitives are not known to the kernel and do not consume any system resources.
MT features, unique to Solaris, are accessible by using native methods.
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 ability to define bound or unbound threads for user or system level control of application concurrency. Note that a bound Java thread can be created only by way of native methods.
In addition, the application can control application concurrency through a programmatic interface. See "Programming Compute-Bound, Parallellized Java Applications" for more information.
The ability to bind a user-level thread (through native methods) to an LWP that is dedicated to a single processor. This feature is useful to real-time applications running on multiprocessor systems.
Synchronization primitives that have interprocess scopes
Synchronization primitives that can be placed in files and can have lifetimes beyond that of the creating thread.
Direct native support for Java's daemon threads. Daemon threads are threads that run in the background and have dedicated exit semantics enabling them to terminate independently of the processes that use them. Daemon threads are useful to libraries that need to create threads that are unknown to applications. The Solaris JVM does not utilize direct native support for Java's daemon threads, but might do so eventually.
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.
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.
This section discusses Java-generic and Solaris-specific issues that might be of concern if you are writing Java applications for the Solaris product.
Numerous methods have been deprecated for JDK 1.1. Refer to Table 4-1 for a complete list.
Some issues are specific to Solaris, as explained in the following sections.
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.
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.
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.