Do you know how to avoid deadlocks in your program? Yes, this is taught, they ask about it at interviews ... And yet, interlocks are found even in popular projects of serious companies like Google. And in Java there is a special class of deadlocks associated with the initialization of classes, sorry for the pun. Such errors are easy to make, but hard to catch, especially since the virtual machine itself is misleading the programmer.
Today we will talk about interlocks during class initialization. I will tell you what it is, illustrate it with examples from real projects, find a bug in the JVM along the way, and show you how to prevent such locks in your code.
')
Deadlock without locks
If I ask you to give an example of interlocking in Java, most likely I will see the code with a
synchronized pair or
ReentrantLock . What about deadlock without
synchronized and
java.util.concurrent at all ? Believe me, it is possible, and in a very concise and straightforward way:
static class A { static final B b = new B(); } static class B { static final A a = new A(); } public static void main(String[] args) { new Thread(A::new).start(); new B(); }
The fact is that according to
§5.5 of the JVM specification , each class has a unique
initialization lock , which is captured during initialization. When another thread tries to access the class being initialized, it will be blocked on this lock until initialization is completed by the first thread. With a competitive initialization of several classes referring to each other, it is easy to stumble on a mutual block.
This is exactly what happened, for example, in the QueryDSL project:
public final class Ops { public static final Operator<Boolean> EQ = new OperatorImpl<Boolean>(NS, "EQ"); public static final Operator<Boolean> NE = new OperatorImpl<Boolean>(NS, "NE"); ...
public final class OperatorImpl<T> implements Operator<T> { static { try {
During the
discussion at StackOverflow, the reason was found, the problem was
reported to the developer , and the bug has now been fixed.
Chicken and egg problem
Exactly the same deadlock can occur whenever a child instance is created in a static class initializer. In fact, this is a special case of the problem described above, since the initialization of the child automatically leads to the initialization of the parent (see
JVMS §5.5 ). Unfortunately, such a pattern can be found quite often, especially when the parent class is abstract:
public abstract class ImmutableList<E> ... { private static final ImmutableList<Object> EMPTY = new RegularImmutableList<Object>(ObjectArrays.EMPTY_ARRAY);
This is a real piece of code from the Google Guava library. Thanks to him, part of our servers after the next update tightly hung up at startup. As it turned out, the reason for this was the Guava update from version 14.0.1 to 15.0, where the unfortunate static static initialization pattern appeared.
Of course, we
reported an error , and after a while it was fixed in the repository, but be careful: the last public release of Guava 18.0 at the time of this writing still contains an error!
One line
Java 8 gave us Stream and Lambda, and with them a new headache. Yes, now it is possible to draw up a whole algorithm with one line in a functional style. But at the same time it is possible and just one line, shoot yourself in the foot.
Want an exercise test? I made a program that calculates the sum of a series; What will it print?
public class StreamSum { static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt(); public static void main(String[] args) { System.out.println(SUM); } }
Now remove
.parallel () or, alternatively, replace the lambda with
Integer :: sum - will anything change?
So what's the deal?Here again there is a deadlock. Thanks to the parallel () directive, stream convolution is performed in a separate thread pool.
From these streams, the body of the lambda is now called, which is written in bytecode as a special private static method inside the same StreamSum class. But this method cannot be called until the static class initializer completes, which in turn waits for the convolution calculation.
More hellThe brain completely blows up that the above fragment works differently in different environments. On a single-processor machine, it will work correctly, and on a multiprocessor, it will most likely freeze. The reason lies in the mechanics of parallelism of the standard Fork-Join pool.
Check it yourself by running an example with different values.
-Djava.util.concurrent.ForkJoinPool.common.parallelism = N
Crafty Hotspot
Normally, deadlocks are easy to detect from Thread Dump: problem threads will hang in the state of
BLOCKED or
WAITING , and the JVM will show in monitors what monitors this or that thread is holding and which ones it is trying to capture. Is this the case with our examples? Take the very first one, with classes A and B. Wait for the hangup and remove the thread dump (using the
jstack utility or using the
Ctrl + \ keys in Linux or the
Ctrl + Break in Windows):
"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001a098800 nid=0x1cf8 in Object.wait() [0x000000001a95e000] java.lang.Thread.State: RUNNABLE at Example1$A.<clinit>(Example1.java:4) at Example1$$Lambda$1/1418481495.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "main" #1 prio=5 os_prio=0 tid=0x000000000098e800 nid=0x23b4 in Object.wait() [0x000000000228e000] java.lang.Thread.State: RUNNABLE at Example1$B.<clinit>(Example1.java:8) at Example1.main(Example1.java:13)
Here are our streams. Both are stuck inside the static initializer
<clinit> , but both are
RUNNABLE ! Somehow it does not fit in with common sense, does the JVM not deceive us?
The feature of
initialization lock is that it is not visible from the Java program, and the capture and release occurs inside the virtual machine. Strictly speaking, according to the
Thread.State specification, neither
BLOCKED (because there is no
synchronized block), nor
WAITING (since the
Object.wait ,
Thread.join and
LockSupport.park methods are not called here) can be here. Moreover,
initialization lock does not need to be a Java object at all. Thus, the only formally admissible state remains
RUNNABLE .
On this subject there is a long-standing bug
JDK-6501158 , closed as
“Not an issue” , and David Holmes himself admitted to me in his correspondence that he has neither the time nor the desire to return to this issue.
If the unobvious state of the stream can still be considered a "feature", then another feature of
initialization lock cannot be called anything other than a "bug." Dealing with the problem, I stumbled upon the source code of HotSpot for strangeness in sending JVMTI notifications: the
MonitorWait event
is sent from the
JVM_MonitorWait function corresponding to the Java method
Object.wait , while the
MonitorWaited symmetrical event
is sent from the low-level
ObjectMonitor :: wait .
As we have already found out, the
Object.wait method
is not called to wait for
initialization lock , so we will not see
MonitorWait events for them, but
MonitorWaited will come, as it is for regular Java monitors, which is not logical.
Found a bug - tell the developer. We follow this rule:
JDK-8075259 .
Conclusion
To ensure thread-safe initialization of classes, the JVM uses synchronization on the invisible programmer's
initialization lock that each class has.
Inaccurate writing of initializers can lead to deadlocks. To avoid this
- ensure that static initializers do not refer to other uninitialized classes;
- do not create instances of child classes in static initializers;
- do not create threads and avoid concurrent code execution in static initializers;
- trust no one; study the source code and report errors found in third-party projects.
According to the analysis of the initialization deadlock, errors were found in
Querydsl ,
Guava and
HotSpot JVM .