As we know, ReentrantLock
has a max reentrant limit: Integer.MAX_VALUE
; Does synchronized
block have reentrant limit too?
Update: I found it is hard to write test code for synchronized reentrant:
public class SyncReentry {
public static void main(String[] args) {
synchronized (SyncReentry.class) {
synchronized (SyncReentry.class) {
// ...write synchronized block for ever
}
}
}
}
Can anyone help write some code for synchronized reentrant limit test?
synchronized block are reentrant in nature i.e if a thread has lock on the monitor object and if another synchronized block requires to have the lock on the same monitor object then thread can enter that code block.
Synchronized blocks in Java are reentrant. This means, that if a Java thread enters a synchronized block of code, and thereby take the lock on the monitor object the block is synchronized on, the thread can enter other Java code blocks synchronized on the same monitor object.
A Java synchronized block doesn't allow more than one JVM, to provide access control to a shared resource. The system performance may degrade because of the slower working of synchronized keyword. Java synchronized block is more efficient than Java synchronized method.
Only one thread per instance can execute inside a synchronized instance method.
Since the specification does not define a limit, it’s implementation specific. There doesn’t even have to be a limit at all, but JVMs are often optimized for high performance, considering the ordinary use cases rather than focusing on support for extreme cases.
As said in this answer, there’s a fundamental difference between an object’s intrinsic monitor and a ReentrantLock
, as you can acquire the latter in a loop, which makes it necessary to specify that there’s limit.
Determining the actual limit of a particular JVM implementation, like the widely used HotSpot JVM, has the problem that there are several factors which can affect the result, even in the same environment.
To experiment with the actual implementation, I used the ASM library to generate bytecode which acquires an object’s monitor in a loop, an action, ordinary Java code can not do
package locking;
import static org.objectweb.asm.Opcodes.*;
import java.util.function.Consumer;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
public class GenerateViaASM {
public static int COUNT;
static Object LOCK = new Object();
public static void main(String[] args) throws ReflectiveOperationException {
Consumer s = toClass(getCodeSimple()).asSubclass(Consumer.class)
.getConstructor().newInstance();
try {
s.accept(LOCK);
} catch(Throwable t) {
t.printStackTrace();
}
System.out.println("acquired "+COUNT+" locks");
}
static Class<?> toClass(byte[] code) {
return new ClassLoader(GenerateViaASM.class.getClassLoader()) {
Class<?> get(byte[] b) { return defineClass(null, b, 0, b.length); }
}.get(code);
}
static byte[] getCodeSimple() {
ClassWriter cw = new ClassWriter(0);
cw.visit(49, ACC_PUBLIC, "Test", null, "java/lang/Object",
new String[] { "java/util/function/Consumer" });
MethodVisitor con = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
con.visitCode();
con.visitVarInsn(ALOAD, 0);
con.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
con.visitInsn(RETURN);
con.visitMaxs(1, 1);
con.visitEnd();
MethodVisitor method = cw.visitMethod(
ACC_PUBLIC, "accept", "(Ljava/lang/Object;)V", null, null);
method.visitCode();
method.visitInsn(ICONST_0);
method.visitVarInsn(ISTORE, 0);
Label start = new Label();
method.visitLabel(start);
method.visitVarInsn(ALOAD, 1);
method.visitInsn(MONITORENTER);
method.visitIincInsn(0, +1);
method.visitVarInsn(ILOAD, 0);
method.visitFieldInsn(PUTSTATIC, "locking/GenerateViaASM", "COUNT", "I");
method.visitJumpInsn(GOTO, start);
method.visitMaxs(1, 2);
method.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
}
On my machine, it printed
java.lang.IllegalMonitorStateException
at Test.accept(Unknown Source)
at locking.GenerateViaASM.main(GenerateViaASM.java:23)
acquired 62470 locks
in one run, but different numbers in the same order of magnitude in other runs. The limit we’ve hit here, is not a counter, but the stack size. E.g. re-running this program in the same environment, but with the -Xss10m
option, gave ten times the number of lock acquisitions.
So the reason why this number is not the same in every run, is the same as elaborated in Why is the max recursion depth I can reach non-deterministic? The reason why we don’t get a StackOverflowError
is that the HotSpot JVM enforces structured locking, which means that a method must release the monitor exactly as often as it has acquired it. This even applies to the exceptional case and as our generated code does not make any attempt to release the monitor, the StackOverflowError
gets shadowed by an IllegalMonitorStateException
.
Ordinary Java code with nested synchronized
blocks can never get anywhere near 60,000 acquisitions in one method, as the bytecode is limited to 65536 bytes and it takes up to 30 bytes for a javac
compiled synchronized
block. But the same monitor can get acquired in nested method invocations.
For exploring the limits with ordinary Java code, expanding the code of your question is not so hard. You just have to give up indenting it:
public class MaxSynchronized {
static final Object LOCK = new Object(); // potentially visible to other threads
static int COUNT = 0;
public static void main(String[] args) {
try {
testNested(LOCK);
} catch(Throwable t) {
System.out.println(t+" at depth "+COUNT);
}
}
private static void testNested(Object o) {
// copy as often as you like
synchronized(o) { synchronized(o) { synchronized(o) { synchronized(o) {
synchronized(o) { synchronized(o) { synchronized(o) { synchronized(o) {
synchronized(o) { synchronized(o) { synchronized(o) { synchronized(o) {
synchronized(o) { synchronized(o) { synchronized(o) { synchronized(o) {
COUNT ++;
testNested(o);
// copy as often as you copied the synchronized... line
} } } }
} } } }
} } } }
} } } }
}
}
The method will invoke itself to have a number of nested acquisitions matching the number of nested invocation times the number of nested synchronized
blocks within method.
When you run it with the small number of synchronized
blocks as above, you’ll get a StackOverflowError
after a large number of invocations, which changes from run to run and is affected by the presence of options like -Xcomp
or -Xint
, indicating that it subject to the indeterministic stack size mentioned above.
But when you raise the number of nested synchronized
blocks significantly, the number of nested invocations becomes smaller and stable. On my environment, it produced a StackOverflowError
after 30 nested calls when having 1,000 nested synchronized
blocks and 15 nested calls when having 2,000 nested synchronized
blocks, which is pretty consistent, indicating that the method invocation overhead has become irrelevant.
This implies more than 30,000 acquisitions, roughly half the number achieved with the ASM generated code, which is reasonable considering that the javac
generated code will ensure a matching number of acquisitions and releases, introducing a synthetic local variable holding the reference of the object that must be released for each synchronized
block. This additional variable reduces the available stack size. It’s also the reason why we now see the StackOverflowError
and no IllegalMonitorStateException
, as this code correctly does structured locking.
Like with the other example, running with larger stack size raises the reported number, scaling linearly. Extrapolating the results implies that it would need a stack size of several GB to acquire the monitor Integer.MAX_VALUE
times. Whether there is a limiting counter or not, becomes irrelevant under these circumstances.
Of course, these code examples are so far away from real life application code, that it should not be surprising that not much optimizations happened here. For real life application code, lock elimination and lock coarsening may happen with a much higher likelihood. Further, real life code will do actual operations needing stack space on their own, rendering the stack requirements of synchronization negligible, so there’s no practical limit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With