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

问题描述

众所周知,ReentrantLock 有一个最大可重入限制:Integer.MAX_VALUE;synchronized 块是否也有可重入限制?

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?

推荐答案

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

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.

正如this answer中所说,对象的内在监视器和ReentrantLock之间存在根本区别code>,因为你可以在循环中获取后者,这使得有必要指定有限制.

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.

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

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.

  • 当 JVM 可以证明对象是纯本地的时,JVM 可能会消除锁,即不同的线程不可能在其上同步
  • JVM可能会在使用同一个对象时合并相邻和嵌套的同步块,这可能在内联后应用,因此这些块在源代码中不需要出现嵌套或彼此靠近
  • JVM 可能有不同的实现,根据对象的类的形状(某些类更可能用作同步键)和特定获取的历史(例如使用偏向锁定,或使用乐观或悲观锁定)来选择方法,取决于锁被争用的频率)

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

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

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

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.

所以这个数字每次运行都不一样的原因,和为什么最大递归深度I中阐述的一样可以达到非确定性?我们没有得到 StackOverflowError 的原因是 HotSpot JVM 强制执行 结构化锁定,这意味着方法必须释放监视器的频率与它获得它的频率完全相同.这甚至适用于例外情况,因为我们生成的代码不会尝试释放监视器,所以 StackOverflowError 会被 IllegalMonitorStateException 遮蔽.

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.

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

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.

对于探索普通 Java 代码的限制,扩展您的问题的代码并不难.你只需要放弃缩进它:

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.

当你像上面那样用少量的 synchronized 块运行它时,你会在大量调用之后得到一个 StackOverflowError,它会随着运行而变化并且受到 -Xcomp-Xint 等选项的影响,表明它受上述不确定堆栈大小的影响.

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.

但是当你显着增加嵌套 同步 块的数量时,嵌套调用的数量会变得更小且稳定.在我的环境中,当有 1,000 个嵌套 synchronized 块时,它在 30 次嵌套调用后产生 StackOverflowError,当有 2,000 个嵌套 synchronized 块时,它产生 15 次嵌套调用,非常一致,说明方法调用开销已经无关紧要了.

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.

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

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.

与另一个示例一样,以更大的堆栈大小运行会提高报告的数字,并线性扩展.推断结果意味着它需要几个 GB 的堆栈大小才能获取监视器 Integer.MAX_VALUE 次.在这些情况下,是否有限制计数器变得无关紧要.

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:35