What about terminating processes which spawn threads?

The last section showed us how to terminate our pseudo-process. But it applied only to a single-threaded process. If a pseudo-process spawns threads, the terminate() method will only terminate the main thread, leaving other rogue threads still running. Fortunately, Java provides a simple way to handle all the threads the application will spawn: the ThreadGroup class. When a Thread is created, it is automatically a member of a ThreadGroup, and new ThreadGroups are normally created as members of the ThreadGroup holding the current thread. So if we create a new ThreadGroup for the pseudo-process' startup thread, all of the threads spawned by the pseudo-process will be in that ThreadGroup. Calling ThreadGroup.stop() will stop all the threads in that ThreadGroup, and all subgroups. The changes needed are relatively simple. We start the class with its own ThreadGroup (new lines of code emphasized):

There are two unlikely but possible problems with our new ability to terminate a multithreaded pseudo-process. Firstly, it may be possible for the pseudo-process to access ThreadGroups that do not belong it, thus allowing the creation of threads that would not be terminated. Currently, I believe this could only be achieved from a custom SecurityManager. In any case, if it is possible, it would be very difficult to achieve, and the vast majority of applications can be reliably assumed to contain only threads that will be stopped when the main thread's ThreadGroup is stopped.

The second problem is associated with the deprecation of Thread.stop(). Now that we are considering multiple threads, it is possible that while terminating the threads, some of the threads will be terminated, leaving the pseudo-process application with a corrupt state just before the remaining threads are terminated. This is unlikely, and even if it did occur, we can assume that it doesn't matter if the application, in its last few milliseconds prior to forced termination, is in a corrupt state. In fact, the application is probably already in an unexpected state, since we are being forced to terminate it.

What if an application calls System.exit()?

We still have one big hole in our multiprocess library. If any application calls System.exit(), the JVM terminates, and all the pseudo-processes will be destroyed with no warning. Fortunately, Java's design once again comes to our aid. Any call to System.exit() is first checked by the SecurityManager to see if the application has permission to terminate the JVM. We can install our own SecurityManager to catch the System.exit() call, disallow it, and terminate the pseudo-process instead. The SecurityManager is actually quite simple to define:

In addition, the SecurityManager should define all other checks so that they do not block pseudo-processes from running. A simple null call for all check* methods will work. We install our own SecurityManager by calling System.setSecurityManager(), i.e., by adding the following line near the startup of the multiprocess library:

System.setSecurityManager(new ExitCatchingSecurityManager());

The Process.terminateProcessWithThreadGroup() method is simple to define, by holding a collection of Process objects in the Process class, searching the collection to find the Process with the identical ThreadGroup, then terminating that Process.

Classpaths, class names, and class versions

Our multiprocess library now seems to provide everything needed to handle multiple Java processes in one JVM. However, JVMs are usually started with a particular classpath, which gives access to all classes. Are we restricted to having only one classpath for our multiprocess JVM? If so, all the classes will have to be in that classpath, and any name clashes from different applications will cause huge problems, as will new versions of a class created after an older version has already been loaded.

This particular problem has been discussed many times for server and development environments. The solution is to dedicate a separate ClassLoader for each pseudo-process. Having a dedicated ClassLoader avoids any name clashes and allows classpaths to be defined at pseudo-process startup. Classes are identified in a JVM by their full name, and also by the ClassLoaders that loaded them. Classes loaded by different ClassLoader instances are separate classes, even if they have the same name. In fact, the same class file loaded by two different ClassLoaders can result in two different classes in the JVM.

We need to make the following changes to our multiprocess library in order to support dedicated per process ClassLoaders. First we need to define a ClassLoader. Then we need to change the code that looks for the class to use our custom ClassLoader. The latter change is quite simple; we go back to the startAnotherClass() method, the very first method defined in this article, and change the line calling Class.forName():

The custom ClassLoader could be more challenging to implement. But in fact, we don't have to work hard to define a proprietary ClassLoader, since Java 2 comes with several useful ClassLoaders. In particular, the java.net.URLClassLoader allows for a complete specification of the classpath. Converting a standard classpath to a list of URLs is easier than defining a new ClassLoader that supports file systems and jar files. The following is a simple implementation of the MyClassLoader class. For simplicity, I've restricted this ClassLoader to allow only one extra directory to be added to the classpath:

Note that there are now two types of classes loaded by the multiprocess library:

Shared classes, which are found in the classpath set when the JVM is started.

Unshared classes loaded separately for each pseudo-process, which are found from the classpath of the per-process classloader.

Having a distinction between shared and unshared classes requires some management, but has the advantage that you can avoid loading the same class multiple times for each process, thus saving more memory. If the management overhead is an issue, you can restrict the shared classes to the SDK classes, and have all application classes unshared.

Echidna

When I first started researching this article, I hadn't encountered Echidna. So I was hugely interested to find this free, open source library which already covered all the issues presented in this article, and many more. Other issues considered by Echidna include:

Starting non-public classes which contain main(String[]) methods

java.lang.Process-like methods for the pseudo-processes

Other SecurityManager requirements

Priority levels of threads to ensure the process manager always has priority

ClassLoaders with multiple classpath entries

Managing shared and non-shared classes

Redirecting console I/O for different processes

A UI to view and manage pseudo-processes

Starting pseudo-processes from the command line while other pseudo-processes are already running