synchronized块是否具有最大重入限制?

Jas*_*son 6 java synchronization reentrantlock java-8

我们知道,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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Hol*_*ger 6

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

正如在这个答案中所说,对象的固有监视器和 a 之间存在根本区别ReentrantLock,因为您可以在循环中获取后者,这使得有必要指定存在限制。

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

  • 当 JVM 可以证明对象是纯本地对象时,它可能会消除锁,即不同的线程不可能在其上同步
  • JVM在使用同一个对象时可能会合并相邻和嵌套的synchronized块,内联后可能会应用,所以这些块不需要在源代码中出现嵌套或彼此靠近
  • 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的机器上,它打印

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

所以这个数字在每次运行中都不相同的原因与为什么我可以达到的最大递归深度不确定?我们没有得到 a 的原因StackOverflowError是 HotSpot JVM 强制执行结构化锁定,这意味着方法必须与获取监视器的频率完全一样频繁地释放监视器。这甚至适用于特殊情况,并且由于我们生成的代码不会尝试释放监视器,因此StackOverflowError会被IllegalMonitorStateException.

带有嵌套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
        } } } }
        } } } }
        } } } }
        } } } }
    }
}
Run Code Online (Sandbox Code Playgroud)

该方法将调用自身以获得与嵌套调用次数乘以synchronized方法内嵌套块数相匹配的嵌套获取次数。

当你用上面的少量synchronized块运行它时,你会StackOverflowError在大量调用后得到一个,它会随着运行而变化,并且受到诸如-Xcomp或等选项的影响-Xint,表明它受不确定性堆栈的影响上面提到的尺寸。

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

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

与另一个示例一样,以更大的堆栈大小运行会提高报告的数量,并线性缩放。推断结果意味着需要几个 GB 的堆栈大小来获取监控Integer.MAX_VALUE时间。在这些情况下,是否有限制计数器变得无关紧要。

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


Thi*_*ilo 2

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

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

好吧,首先,它已经足够了……但这将通过重入计数器来实现,这些东西最终会溢出。