Class-loader is an Object that is responsible for loading classes. Class-loaders locate the bytecode for a particular class, then transform that bytecode into a usable class in the runtime system. Java introduced parallel class-loaders which are multi-threaded. While loading the classes in parallel can improve performance, it can also lead to deadlocks if you are not careful. In Java 7 release, Oracle did important enhancements to avoid deadlocks.
Multi-threading applications are notoriously hard to get right. In this blogpost, I’ll take you through some code samples that can lead to deadlock when using parallel class-loaders.
Before we get to the code samples, it is important to understand how a class or interface is initialized in Java. Per Java specification, a class or interface type T will be initialized immediately before the first occurrence of any one of the following:
- T is a class and an instance of T is created.
- T is a class and a static method declared by T is invoked.
- A static field declared by T is assigned.
- A static field declared by T is used and the field is not a constant variable.
- T is a top level class, and an assert statement lexically nested within T is executed.
During the class initialization, class-loaders initialize static fields. As per Java specification, static field initialization is thread-safe. Class-loaders generally use locks while loading and initializing a class for thread-safety. The granularity of the locks are left to the implementation as long as they are thread safe.
Class-loaders that are not multi-threaded or parallel capable, use a single lock to load all the classes. This means, only one class can be loaded at any point in time. Class-loaders that are multi-threaded or parallel capable use one lock per class or interface. AppClassLoader is a default application class-loader that is multi-threaded. AppClassLoader uses a concurrent hash map to maintain a unique lock per class/interface. This is where things go wrong!
Consider the following code sample:
package net.chandrat;
public class Cross {
static class A {
public final static B b = new B();
public void randomA() {
System.out.println("randomA...");
}
};
static class B {
public final static A a = new A();
public void randomB() {
System.out.println("randomB...");
}
}
public static void main(String[] args) {
System.out.println("starting...");
System.out.println(Cross.class.getClassLoader());
new Thread(new Runnable() {
@Override
public void run() {
A.b.randomB();
}
}).start();
B.a.randomA();
System.out.println("ending...");
}
}
In the above code sample, you have two class A and B. Class A, has a static member b that is initialized to an instance of B. Likewise, class B has a static member a that is initialized to an instance of A. According to the Java specification, when a class is loaded, the statics are initialized in thread safe manner.
When you run this program, Java by default uses AppClassLoader to load the classes A and B. In the above code sample, there are two threads that are trying to load classes A & B.
According to the implementation of AppClassLoader, when thread T1 tries to load class A, it acquires a lock for class A and tries to load A. To complete loading class A, it needs to initialize the static member of A with instance of class B. Imagine right at that point, another thread T2, tries to load class B. T2 acquires the lock for class B. But to complete loading of class B, it should initialize the static member with instance of class A. At that point, both threads T1 and T2 will be waiting for lock on class B and class A respectively causing a deadlock.
Order of events in sequence:
- T1 → tries to load class A.
- T1 → acquires lock for class A.
- T2 → tries to load class B.
- T2 → acquires lock for class B.
- T1 → tries to initialize static field b. To finish initialization it needs to load class B. To load class B, it requires a lock for class B. T1 waits to acquire lock on B to complete loading class A.
- T2 → tries to initialize static field a. To finish initialization it needs to load class A. To load class A, it requires a lock for class A. T2 waits to acquire lock on B to complete loading class B.
At this point both T1 & T2 are deadlocked.
If AppClassLoader was not multi-threaded, deadlock would never have happened. Having different behavior with parallel capable class loaders and serial class loaders is not acceptable. Initialization process in the Java spec (refer jls section 12.4.2) should be fixed to avoid deadlocks with multi-threaded class-loaders.
To summarize, parallel class-loader can cause deadlocks and it is important to understand the implications to avoid code that could cause deadlocks. Deadlocks could happen in any case where similar kind of race condition is possible.