JDK-4891511 : Deadlock in class initialization specification, JLS 2nd ed. 12.4.2
  • Type: Enhancement
  • Component: specification
  • Sub-Component: language
  • Affected Version: 1.4.1
  • Priority: P3
  • Status: Closed
  • Resolution: Not an Issue
  • OS: windows_nt
  • CPU: x86
  • Submitted: 2003-07-16
  • Updated: 2008-02-08
  • Resolved: 2008-02-08
Related Reports
Relates :  
Description

Name: gm110360			Date: 07/16/2003


FULL PRODUCT VERSION :
java version "1.4.1"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.1-b21)
Java HotSpot(TM) Client VM (build 1.4.1-b21, mixed mode)


FULL OPERATING SYSTEM VERSION :
Windows NT Version 4.0
service pack 6

A DESCRIPTION OF THE PROBLEM :
When a superclass contains a static reference to one of its
subclasses, a deadlock can occur during class
initialization.  This happens when one thread initializes
the parent class and another thread initializes the child
class, near the same time.

This is actually a bug in the algorithm given for class
initialization in section 12.4.2 of the Java Language
Specification, second edition.  That is, the JVM appears to
implement the spec as written; but the spec has this
inherent deadlock in its algorithm.

I propose the following change to the algorithm, which
eliminates the deadlock by always setting locks from
superclass to subclass.  (It will not eliminate all possible
class-initialization deadlocks, because two arbitrary
classes can statically refer to each other.  But it will
eliminate the superclass-subclass deadlock illustrated by
the accompanying code.)  Replace steps 6 and 7 of the
algorithm with the following:

6. If the Class object represents a class rather than an
interface, and the superclass of this class has not yet been
initialized, then release the lock on this Class object and
recursively perform this entire procedure for the
superclass. If necessary, verify and prepare the superclass
first. If the initialization of the superclass completes
abruptly because of a thrown exception, then lock this Class
object, label it erroneous, notify all waiting threads,
release the lock, and complete abruptly, throwing the same
exception that resulted from initializing the superclass.
If the initialization of the superclass completes normally,
then return to step 1.  Note that this step will not be
repeated the next time through.

[Also note that the check of whether the superclass has been
initialized already must be done without locking the
superclass.  Since this is a boolean, no lock is required
for safety.]

7. Otherwise, record the fact that initialization of the
Class object is now in progress by the current thread and
release the lock on the Class object.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Compile and run the accompanying code
2. Note that it does not complete.
3. Interrupt to get a thread dump, and note where it is
deadlocked.

EXPECTED VERSUS ACTUAL BEHAVIOR :
I would expect this output from the sample code:

Initializing A from thread Thread-1
Initializing B from thread Thread-1
In A.method() from thread Thread-1
In B.method() from thread Thread-2


What I actually see is just the first line, followed by an
indefinite hang.  Generating a thread dump I see:

Full thread dump Java HotSpot(TM) Client VM (1.4.1-b21 mixed
mode):

"DestroyJavaVM" prio=5 tid=0x00773C80 nid=0x156 waiting on
condition [0..6fad8]

"Thread-2" prio=5 tid=0x00773EA0 nid=0x1dd in Object.wait()
[ae3f000..ae3fd8c]
	at ClinitDeadlock$2.run(ClinitDeadlock.java:10)

"Thread-1" prio=5 tid=0x00771070 nid=0x185 in Object.wait()
[adff000..adffd8c]
	at A.<clinit>(A.java:18)
	at ClinitDeadlock$1.run(ClinitDeadlock.java:7)

"Signal Dispatcher" daemon prio=10 tid=0x0076C6A0 nid=0x23d
waiting on condition [0..0]

"Finalizer" daemon prio=9 tid=0x007686A0 nid=0xd5 in
Object.wait() [acbf000..acbfd8c]
	at java.lang.Object.wait(Native Method)
	- waiting on <02B60498> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:111)
	- locked <02B60498> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:127)
	at
java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x007677B0 nid=0x1d3
in Object.wait() [ac7f000..ac7fd8c]
	at java.lang.Object.wait(Native Method)
	- waiting on <02B60388> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:426)
	at
java.lang.ref.Reference$ReferenceHandler.run(Reference.java:113)
	- locked <02B60388> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00766AA0 nid=0x24f runnable

"VM Periodic Task Thread" prio=10 tid=0x0076BBA0 nid=0x19c
waiting on condition
"Suspend Checker Thread" prio=10 tid=0x0076B160 nid=0x245
runnable


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
I have three files, ClinitDeadlock.java, A.java, and B.java.

ClinitDeadlock.java:

public class ClinitDeadlock
{
    public static void main( String[] args ) {
        new Thread() {
            public void run() { A.method(); }
        }.start();
        new Thread() {
            public void run() { B.method(); }
        }.start();
    }
}


A.java:

public abstract class A
{
    public static void method() {
        System.out.println( "In A.method() from thread "
                            +Thread.currentThread().getName() );
    }

    static {
        System.out.println( "Initializing A from thread "
                            +Thread.currentThread().getName() );
        try {
            Thread.sleep( 1000 );
        }
        catch (InterruptedException e) {}
    }

    public static final A globalA = new B();
}


And B.java:

public class B extends A
{
    public static void method() {
        System.out.println( "In B.method() from thread "
                            +Thread.currentThread().getName() );
    }

    static {
        System.out.println( "Initializing B from thread "
                            +Thread.currentThread().getName() );
        try {
            Thread.sleep( 1000 );
        }
        catch (InterruptedException e) {}
    }
}

---------- END SOURCE ----------

CUSTOMER WORKAROUND :
The only reliable workaround is never to statically refer to
child classes from the parent.  One could argue that it is
somehow inherently bad design to do so; however, there are
cases where this is the most elegant design possible.  The
best example of this I know of is a family of classes
representing intervals.  The root of the family is an
abstract class; in the original design the root class
included a couple of constants for the empty interval and
the "everything" interval, which were instances of a
concrete subclass.  The root class is in fact the best place
for these constants, but this bug forced a workaround of
putting the constants in the concrete subclass.
(Incident Review ID: 166849) 
======================================================================

Comments
EVALUATION * The scenario First in thread T1: Lock A.class Mark A as under initialization by T1 Unlock A.class Execute A's static initializer; sleep for a bit Now thread T2 gets to run: Lock B.class Mark B as under initialization by T2 Unlock B.class // Realize (super)class A has not been initialized Lock A.class (1) // Realize initialization of A is in progress elsewhere Unlock A.class and loop to (1) Suppose T1's sleep is now over, so back to initialization of A: Execute class variable initializer for A.globalA = new B() // Realize class B has not been initialized Lock B.class (2) // Realize initialization of B is in progress elsewhere Unlock B.class and loop to (2) * The problem For mutually referring classes in the same thread, the algorithm works, since step 3 refuses to wait for anything. For mutually referring classes in different threads, the algorithm doesn't work because of the deadlock above. It doesn't matter that B is a subclass of A - if B wasn't a subclass, it could still have a class variable initializer of 'new A()', which would make T2 realize that A has not been initialized, so the deadlock would still start. * The proposal "6. If the Class object represents a class rather than an interface, and the superclass of this class has not yet been initialized, then release the lock on this Class object and recursively perform this entire procedure for the superclass. ... If the initialization of the superclass completes normally, then return to step 1. Note that this step will not be repeated the next time through. [Also note that the check of whether the superclass has been initialized already must be done without locking the superclass. Since this is a boolean, no lock is required for safety.] 7. Otherwise, record the fact that initialization of the Class object is now in progress by the current thread and release the lock on the Class object." When T2 first runs, the new step 6 would unlock B.class and try to initialize A. (The [...] text is irrelevant because the existing step 6 checks the state of the superclass without taking a lock.) But in performing initialization recursively for A, T2 returns to step 1 and locks A.class and finds T1 initializing A. So T2 waits, and when T1 runs, there will be deadlock because T2 is still recorded as initializing B. Maybe the proposer doesn't care about A's initialization by T1 and thinks T2 should initialize A regardless, because the procedure was started recursively for a superclass? (Hence the algorithm would need to track even more state.) Or, maybe T2 should un-record that it's initializing B, so that T1 can wake up and perform a recursive initialization of B? (But this would allow T1 to initialize B at the same time as T2 initializes A, which seems undesirable.) It is not obvious that the proposal is actually an improvement even in the case of superclass+subclass, let alone in the general case of mutually referring classes. Adding in the compatibility impact of any change to initialization, I don't think it's prudent to pursue this proposal.
08-02-2008

EVALUATION This bug describes a problem with the contents of the JLS 2nd ed. but maintains that the current spec is properly implemented. Routing to the specification team for evaluation of the class initialization proceedure. Obviously, if the spec is modified, we'll have to update the implemenation. -- iag@sfbay 2003-07-16 The spec has had know problems for a long time. No completely satisfactory solution has ever been proposed. However, the suggestion should be examined carefully to see if the benefits outweigh the inherent costs of meddling with this. ###@###.### 2003-08-22
22-08-2003