由于规范没有定义限制,它是特定于实现的。甚至根本不需要限制,但 JVM 通常会针对高性能进行优化,考虑普通用例而不是专注于对极端情况的支持。
正如this answer 中所说,对象的内在监视器和ReentrantLock 之间存在根本区别,因为您可以在循环中获取后者,因此有必要指定存在限制。
确定特定 JVM 实现的实际限制,例如广泛使用的 HotSpot 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 强制执行结构化锁定,这意味着一个方法必须像它获得它一样频繁地释放监视器。这甚至适用于例外情况,因为我们生成的代码不会尝试释放监视器,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
} } } }
} } } }
} } } }
} } } }
}
}
该方法将调用自身以使嵌套获取的数量与嵌套调用的数量乘以方法内嵌套的synchronized块的数量相匹配。
当你像上面那样用少量的synchronized 块运行它时,你会在大量调用之后得到一个StackOverflowError,它会随着运行的变化而变化,并且会受到@ 等选项的影响987654338@或-Xint,表示它受制于上述不确定的堆栈大小。
但是当您显着增加嵌套synchronized 块的数量时,嵌套调用的数量会变得更小且稳定。在我的环境中,当有 1,000 个嵌套的 synchronized 块时,它在 30 次嵌套调用后产生了 StackOverflowError,当有 2,000 个嵌套的 synchronized 块时,它产生了 15 次嵌套调用,这非常一致,表明方法调用开销已变得无关紧要。
这意味着超过 30,000 次获取,大约是使用 ASM 生成代码实现的数量的一半,考虑到javac 生成的代码将确保获取和释放的数量匹配,引入了一个包含引用的合成局部变量,这是合理的必须为每个synchronized 块释放的对象。这个额外的变量减少了可用的堆栈大小。这也是我们现在看到 StackOverflowError 而没有看到 IllegalMonitorStateException 的原因,因为这段代码正确地进行了结构化锁定。
与另一个示例一样,以更大的堆栈大小运行会提高报告的数字,并线性扩展。推断结果意味着它需要几个 GB 的堆栈大小才能获取监视器Integer.MAX_VALUE 次。在这些情况下,是否有限制计数器变得无关紧要。
当然,这些代码示例与现实生活中的应用程序代码相去甚远,因此这里没有进行太多优化也就不足为奇了。对于现实生活中的应用程序代码,锁消除和锁粗化的可能性要高得多。此外,现实生活中的代码会自行执行需要堆栈空间的实际操作,从而使同步的堆栈需求可以忽略不计,因此没有实际限制。