Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does synchronized block have max reentrant limit?

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?

like image 923
Jason Avatar asked Feb 27 '19 03:02

Jason


People also ask

Are synchronized blocks reentrant?

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.

Are synchronized methods reentrant?

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.

Is synchronized block more efficient than method?

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.

How many threads per instance can execute inside a synchronized?

Only one thread per instance can execute inside a synchronized instance method.


1 Answers

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.

  • The JVM may eliminate locks when it can prove that the object is purely local, i.e. it is impossible that a different thread ever synchronizes on it
  • The JVM may merge adjacent and nested synchronized blocks when they use the same object, which may apply after inlining, so these blocks do not need to appear nested or close to each other in source code
  • The JVM may have different implementations, selected based on the shape of the object’s class (some classes are more likely to be used as synchronization key) and the history of a particular acquisition (e.g. use biased locking, or use optimistic or pessimistic approaches, depending on how often the lock was contended)

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.

like image 65
Holger Avatar answered Oct 06 '22 00:10

Holger