首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >同步块有最大重入限制吗?

同步块有最大重入限制吗?
EN

Stack Overflow用户
提问于 2019-02-27 03:41:54
回答 2查看 643关注 0票数 7

我们知道,ReentrantLock有一个最大的可重入限制:Integer.MAX_VALUEsynchronized块也有可重入限制吗?

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

代码语言:javascript
复制
public class SyncReentry {
    public static void main(String[] args) {
        synchronized (SyncReentry.class) {
            synchronized (SyncReentry.class) {
                // ...write synchronized block for ever
            }
        }
    }
}

有人能帮助为同步重入极限测试编写一些代码吗?

EN

回答 2

Stack Overflow用户

发布于 2019-02-27 15:35:54

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

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

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

  • 当JVM能够证明对象纯粹是本地的时,JVM可以消除锁,也就是说,不可能有不同的线程在其上同步。
  • 当JVM使用相同的对象时,它们可能合并相邻的和嵌套的同步块,这可能在内联之后应用,因此这些块在源代码中不需要显示为嵌套的或相互接近的。
  • JVM可能有不同的实现,根据对象类的形状(一些类更可能用作同步键)和特定获取的历史(例如,使用偏置锁定,或者使用乐观或悲观的方法,这取决于锁被竞争的频率)

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

代码语言:javascript
复制
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();
    }
}

在我的机器上,印着

代码语言:javascript
复制
java.lang.IllegalMonitorStateException
    at Test.accept(Unknown Source)
    at locking.GenerateViaASM.main(GenerateViaASM.java:23)
acquired 62470 locks

在一次运行,但不同的数字在同一数量级在其他运行。我们在这里遇到的限制不是计数器,而是堆栈大小。例如,在相同的环境中重新运行这个程序,但是使用-Xss10m选项,则给出了10倍的锁收购次数。

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

使用嵌套synchronized块的普通Java代码在一种方法中永远无法获得接近60,000次的收购,因为字节码被限制在65536字节以内,而javac编译的synchronized块最多需要30字节。但是,在嵌套的方法调用中可以获得相同的监视器。

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

代码语言:javascript
复制
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个嵌套调用之后生成一个synchronized,当有2,000个嵌套的synchronized块时,生成了15个嵌套调用,这是非常一致的,表明方法调用开销已经变得不相关了。

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

与另一个示例一样,使用较大的堆栈大小运行会增加报告的数量,线性缩放。推断结果意味着需要几个GB的堆栈大小才能获得监视器Integer.MAX_VALUE时间。在这种情况下,是否存在限制计数器,就变得无关紧要了。

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

票数 6
EN

Stack Overflow用户

发布于 2019-02-27 08:42:25

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

为什么线程只支持2,147,483,647,我现在也很好奇!

好吧,首先,这足够了.但是这将通过一个再入计数器来实现,这些东西最终会溢出。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/54897677

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档