同步块是否具有最大可重入限制?

时间:2019-02-27 03:41:54

标签: java java-8 synchronization reentrantlock

我们知道,print('same')的最大进入限制为:ReentrantLockInteger.MAX_VALUE块也有可重入限制吗?

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

synchronized

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

2 个答案:

答案 0 :(得分:4)

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

this answer中所述,对象的内部监视器与ReentrantLock之间存在根本的区别,因为您可以循环获取后者,这使得必须指定存在限制。 / p>

确定某个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选项,可获得十倍的锁获取次数。

因此每次运行该数字都不相同的原因与Why is the max recursion depth I can reach non-deterministic?中详细说明的原因之所以没有得到StackOverflowError,是因为HotSpot JVM强制执行结构化锁定,这意味着方法必须完全按照获取监视器的频率释放监视器。这甚至适用于特殊情况,并且由于我们生成的代码未尝试释放监视器,因此StackOverflowErrorIllegalMonitorStateException遮盖了。

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

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

synchronized

该方法将自身进行调用,以使其具有与方法中嵌套的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块的数量时,嵌套调用的数量将变得更小且稳定。在我的环境中,当有1,000个嵌套synchronized块时,它在30个嵌套调用之后产生了StackOverflowError,而在有2,000个嵌套synchronized块时,它产生了15个嵌套调用,这是非常一致的,表明该方法调用开销变得无关紧要。

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

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

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

答案 1 :(得分:1)

这不是直接的答案,但是由于要在同一监视器上(甚至在不同的监视器上)使很多重入synchronized块中的唯一方法是递归方法调用(您不能以编程方式锁定它例如,在一个紧密循环中),在达到JVM内部为此保留的计数器限制之前,您将用完调用堆栈空间。

  

为什么现在我也想知道为什么线程仅支持2,147,483,647!

首先,足够了... 但这将通过重新输入计数器来实现,这些东西最终会溢出。