本文介绍了同步块是否具有最大可重入限制?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们知道,ReentrantLock具有最大可重入限制:Integer.MAX_VALUE; synchronized区块也有可重入限制吗?

更新:我发现很难为同步可重入编写测试代码:

public class SyncReentry {
    public static void main(String[] args) {
        synchronized (SyncReentry.class) {
            synchronized (SyncReentry.class) {
                // ...write synchronized block for ever
            }
        }
    }
}

任何人都可以帮助编写一些代码以进行同步可重入限制测试吗?

解决方案

由于该规范未定义限制,因此特定于实现.甚至根本没有限制,但是JVM通常针对高性能进行优化,考虑了普通用例,而不是专注于对极端情况的支持.

此答案中所述,对象的内部监视器和ReentrantLock之间存在根本的区别,可以循环获取后者,因此有必要指定限制.

确定某个JVM实现的实际限制(例如广泛使用的HotSpot JVM)存在一个问题,即使在同一环境中,也有几个因素会影响结果.

  • 当JVM可以证明对象是纯粹的本地对象时,即可能不可能有不同的线程在其上进行同步,则JVM可以消除锁定.
  • 当JVM使用同一对象时,JVM可能会合并相邻和嵌套的同步块,这可能在内联之后应用,因此这些块无需在源代码中看起来嵌套或彼此靠近
  • JVM可能具有不同的实现,具体取决于对象类的形状(某些类更可能用作同步密钥)和特定采集的历史记录(例如,使用偏向锁定,乐观或悲观)的方法,取决于锁的争用频率)

为了试验实际的实现,我使用了 ASM 库来生成获取对象监视器的字节码.在一个循环中,一个动作,普通的Java代码做不到

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();
    }
}

在我的机器上,它已打印

 java.lang.IllegalMonitorStateException
    at Test.accept(Unknown Source)
    at locking.GenerateViaASM.main(GenerateViaASM.java:23)
acquired 62470 locks
 

一次运行

,但在另一次运行中以相同的数量级显示不同的数字.我们在这里达到的极限不是计数器,而是堆栈大小.例如.在相同的环境中重新运行该程序,但使用-Xss10m选项,可以获得十倍于锁的获取次数.

因此每次运行中该数字都不相同的原因与详细说明的原因相同可以达到不确定性吗?之所以没有得到StackOverflowError,是因为HotSpot JVM强制执行结构化锁定,这意味着方法必须经常释放监视器.因为它已经获得了它.这甚至适用于特殊情况,并且由于我们生成的代码未尝试释放监视器,因此StackOverflowErrorIllegalMonitorStateException遮盖.

带有嵌套synchronized块的普通Java代码用一种方法永远无法获得近60,000次采集,因为字节码限制为65536字节,而javac编译的synchronized块最多需要30个字节.但是可以在嵌套方法调用中获取同一监视器.

要探索普通Java代码的局限性,扩展问题代码并不难.您只需要放弃缩进即可:

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
        } } } }
        } } } }
        } } } }
        } } } }
    }
}

该方法将自身进行调用,以使其具有与方法中嵌套的synchronized块的嵌套调用次数乘以嵌套的调用次数相匹配的嵌套采集.

当使用上述少量的synchronized块运行它时,经过大量调用后,您会得到一个StackOverflowError,这在每次运行中都会发生变化,并受诸如-Xcomp-Xint,表示它受上述不确定的堆栈大小的限制.

但是,当您大幅增加嵌套的synchronized块的数量时,嵌套调用的数量将变得更小且稳定.在我的环境中,当有1,000个嵌套的synchronized块时,它在30个嵌套调用之后产生了StackOverflowError,而在具有2,000个嵌套的synchronized块时,它产生了15个嵌套调用,这非常一致,这表明方法调用开销变得无关紧要.

这意味着超过30,000次获取,大约是ASM生成的代码完成的数量的一半,考虑到javac生成的代码将确保获取和发布的数量匹配,这是合理的,引入了包含引用的合成局部变量每个synchronized块必须释放的对象的大小.此附加变量减小了可用的堆栈大小.这也是我们现在看到StackOverflowError而没有看到IllegalMonitorStateException的原因,因为此代码正确地执行了结构化锁定.

与其他示例一样,以较大的堆栈大小运行会增加报告的数量,并线性缩放.外推结果意味着它需要几GB的堆栈大小才能获取监视器Integer.MAX_VALUE次.在这种情况下,是否存在限制计数器变得无关紧要.

当然,这些代码示例与现实生活中的应用程序代码相距甚远,因此这里没有发生太多优化也就不足为奇了.对于现实生活中的应用程序代码,锁消除和锁粗化的可能性更高.此外,现实生活中的代码将自行执行需要堆栈空间的实际操作,从而使同步的堆栈要求可忽略不计,因此没有实际限制.

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?

解决方案

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.

这篇关于同步块是否具有最大可重入限制?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

06-16 08:33