From: Godmar Back (gback@marker.cs.utah.edu)
Date: Tue Nov 17 1998 - 03:28:34 EST
Hi, as I'm hacking along, today part two of the story of the red line.
First, the good news: Kaffe now passes the following test.
Take a good look at it before reading on.
-----------------------------------------------------------------------------
/**
* ExceptionInInitializerTest.java
*
* This is a test for some intricacies during class loading and initializing.
*/
/*
* This class will throw an exception when its static initializer is
* executed. Hence, this class is never successfully loaded.
*/
class Throw {
static {
System.out.println("Running static initializer of Throw");
// NullPointerException!!!
System.out.println(((String)null).hashCode());
}
// we use this variable to trigger an active use of this class
static boolean activeUse;
}
/*
* main test class
*/
public class ExceptionInInitializerTest
{
public static void main(String av[]) {
// start watchdog. If this thread times out, we've most
// likely deadlocked.
new Thread(
new Runnable() {
public void run()
{
try { Thread.sleep(5000); } catch (Exception _) {}
System.out.println("Failure due to timeout, exiting");
System.exit(-1);
}
}).start();
System.out.println("Static initializer test");
try {
// trigger the loading of class "Throw" by attempting to use it
Throw.activeUse = true;
// the resulting exception should be caught here
} catch (ExceptionInInitializerError e) {
/*
* Attempt to load and process java.math.BigDecimal from within
* a different thread.
* This checks whether the original thread still has the class
* loading mechanism locked. It mustn't, of course.
*/
Thread th = new Thread(
new Runnable() {
public void run()
{
System.out.println("Now loading " +
java.math.BigDecimal.class.getName());
}
});
th.start();
try { th.join(); } catch (Exception ire) {
System.out.println(ire);
}
// extract the exception that was thrown in the initializer
Throwable t = e.getException();
// it better be a NullPointerException
if (t instanceof NullPointerException) {
System.out.println("Success 1.");
/* Now let's try accessing Throw again.
* The correct error to throw here is NoClassDefFoundError
*/
try {
System.out.println(Throw.activeUse);
} catch (NoClassDefFoundError _) {
System.out.println("Success 2.");
System.exit(0);
}
System.out.println("Failed, was class loaded.");
}
System.out.println(e);
}
System.out.println("Failed, exception wasn't caught");
}
}
-----------------------------------------------------------------------------
The case of calling the static initializer of a class from within
processClass is an example of where a function below the red line
calls a function above the red line. I'll call that an upcall.
I had already pointed out that since below the red line functions
cannot throw exceptions, all exceptions must be caught. I'm using the
JNI interface for that.
Observation 1: An exception thrown and caught in an upcall will propagate
as an error to the caller of processClass. Unfortunately, the information
describing this error is no longer a pair (char *classname, char *mess),
it is instead a pair ("ExceptionInInitializerError", jthrowable).
Hmmm. For now I just use "mess" to hold the jthrowable and special-case it
when I actually construct the exception object.
Observation 2: Since the upcall can take arbitrarily long, it is of
course unacceptable for it to prevent other threads from calling
functions below the red line. Therefore, it is imperative that we
give up any locks we hold before performing the upcall.
To be more general, it is imperative that a below the red line function
allows for it to be reentered before performing an upcall. This may require
saving and restoring state, or simply unlocking and reacquiring a lock.
There is a conclusion 3, which the example does not show.
Consider the case where a class is processed to CSTATE_OK, but it must
first process its superclass to CSTATE_OK. Suppose the superclass
has a static initializer. Clearly, we must not hold the class lock
when executing the superclass's static initializer.
Therefore, we cannot code:
processClass()
{
...
lockStaticMutex(&classlock);
...
if (class->superclass->state < CSTATE_OK) {
processClass(class->superclass, CSTATE_OK);
}
...
}
Instead, we must release and reacquire that lock:
processClass()
{
...
lockStaticMutex(&classlock);
...
if (class->superclass->state < CSTATE_OK) {
unlockStaticMutex(&classlock);
processClass(class->superclass, CSTATE_OK);
lockStaticMutex(&classlock);
}
...
}
Conclusion 3 is then that if a below the red function calls another
below the red line function that may perform an upcall, it must treat the
call as if it made an upcall itself with regard to saving and restoring
its state and preserving its reentrancy.
Now you may say: wait a minute, the purpose of the classLock was to
ensure that no other thread enters processClass while we're shepherding
a class along the loading path. Clearly, that goal is not desirable:
we do not want to deny other threads access to the class loading facilities
just because some class's static initializer takes too long.
Furthermore, what can happen is that another thread might want to
process a class to CSTATE_OK in order to access a static variable, for
instance. This may in fact happen while the loading of the class is
in process. Note that if the thread loading the class holds the class
lock, and the second thread has to wait to get the class lock before
it can even check whether that class needs further loading, then things
will automatically work because the second thread will only see the
result of the loading. But, as we already said, the loading thread
must not hold that lock.
At this point, I'm wondering why processClass takes a global lock at all
as opposed to simply locking the class it is currently dealing with.
I admit I don't know why. In any event, there's two possible solutions:
a) assuming a global lock is held, add a global condition variable on
which additional callers suspend. This global condition variable
is signaled upon return from processClass.
If a thread enters processClass, and notices that the operation
it wanted to do is already in progress by another thread,
it will simply be suspended until the other thread is done.
b) assuming no global lock is needed, simply have the thread doing the
upcall hold a class-local lock which additional caller have to acquire
first. Such a lock could be obtained from the corresponding centry,
for instance.
From these alternatives, I draw conclusion 4:
If it is necessary to block other threads from executing code below
the red line while an upcall is in progress, then do so in a manner that
*only* affects the threads whose progress depends on the completion of
the upcall.
That said: I haven't implemented neither a) nor b) yet (that's why
the example doesn't show that it works yet ;-) If somebody tells or reminds
me of why there is a global classLock (i.e., what data structures it is
supposed to protect) and why a local classLock wouldn't work, I'll implement
solution a). On the other hand, while a condition variable would wake
up all threads waiting on progress in the processing of any class,
the per-class-lock solution would only wake up those threads waiting
on a given class. The price to pay is a (dynamic) lock per class.
I realize that the second part of my discussion went a lot into
the details of processClass's and its locking mechanism.
For this reason, let me repeat the key point I am trying to make here:
the below the red line functions in the VM core must prepare itself and
be able to deal with being reentered by the same or another thread
while making upcalls.
Feedback is welcome.
- Godmar
This archive was generated by hypermail 2b29 : Sat Sep 23 2000 - 19:57:03 EDT