📜 ⬆️ ⬇️

Class about deadlock classes



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 { // initialize all fields of Ops List<Field> fields = new ArrayList<Field>(); fields.addAll(Arrays.asList(Ops.class.getFields())); for (Class<?> cl : Ops.class.getClasses()) { fields.addAll(Arrays.asList(cl.getFields())); } ... 

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 hell
The 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

According to the analysis of the initialization deadlock, errors were found in Querydsl , Guava and HotSpot JVM .

Source: https://habr.com/ru/post/255067/


All Articles