Поскольку спецификация не определяет ограничение, она зависит от реализации.Там даже не должно быть никакого предела, но JVM часто оптимизируются для высокой производительности, учитывая обычные варианты использования, а не фокусируясь на поддержке крайних случаев.
Как сказано в , этот ответ, есть принципиальная разница между встроенным монитором объекта и ReentrantLock
, так как вы можете получить последний в цикле, что делает необходимым указывать, что есть предел.
Определение фактического пределаВ конкретной реализации JVM, например, в широко используемой JVM HotSpot, существует проблема, заключающаяся в том, что существует несколько факторов, которые могут повлиять на результат, даже в одной и той же среде.
- 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
число получений блокировок увеличилось в десять раз.
Поэтому причина, по которой это число не одинаково при каждом запуске, заключается в том, чтокак описано в Почему максимальная глубина рекурсии, которую я могу достичь, недетерминирована? Причина, по которой мы не получаем StackOverflowError
, заключается в том, что JVM HotSpot обеспечивает структурную блокировку ,Это означает, что метод должен освобождать монитор точно так же часто, как он его получил.Это относится даже к исключительному случаю, и поскольку наш сгенерированный код не предпринимает никаких попыток освободить монитор, StackOverflowError
затеняется IllegalMonitorStateException
.
Обычный код Java с вложенными блоками synchronized
можетникогда не получайте около 60 000 запросов одним способом, поскольку байт-код ограничен 65536 байтами и занимает до 30 байтов для скомпилированного javac
блока synchronized
.Но один и тот же монитор может быть получен во вложенных вызовах методов.
Для изучения ограничений с помощью обычного кода 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
после большого количества вызовов, которое меняется от запуска к запуску и зависит от наличия таких параметров, как-Xcomp
или -Xint
, что означает, что он зависит от указанного выше недетерминированного размера стека.
Но когда вы значительно увеличите число вложенных блоков synchronized
, количество вложенных вызовов станет меньше и будет стабильным.В моей среде он выдавал StackOverflowError
после 30 вложенных вызовов при наличии 1000 вложенных блоков synchronized
и 15 вложенных вызовов при наличии 2000 вложенных блоков synchronized
, что довольно непротиворечиво, что указывает на то, что накладные расходы на вызов метода стали неактуальными.
Это подразумевает болееБолее 30 000 приобретений, что примерно вдвое меньше, чем при использовании сгенерированного кода ASM, что разумно, учитывая, что сгенерированный код javac
обеспечит совпадение количества приобретений и выпусков, введя синтетическую локальную переменную, содержащую ссылку на объект, который долженбыть освобожденным за каждый synchronized
блок.Эта дополнительная переменная уменьшает доступный размер стека.Это также причина, по которой мы теперь видим StackOverflowError
, а не IllegalMonitorStateException
, поскольку этот код корректно выполняет структурированную блокировку .
Как и в другом примере, работа с увеличенным размером стека повышаетсообщаемое число, масштабируется линейно.Экстраполяция результатов подразумевает, что для получения монитора Integer.MAX_VALUE
раз потребуется стек в несколько ГБ.Независимо от того, существует ли ограничивающий счетчик или нет, он становится неактуальным в этих обстоятельствах.
Конечно, эти примеры кода настолько далеки от реального прикладного кода, что неудивительно, что здесь произошла небольшая оптимизация.В реальном прикладном коде устранение блокировки и ее огрубление могут происходить с гораздо большей вероятностью.Кроме того, реальный код будет выполнять реальные операции, требующие стекового пространства самостоятельно, что делает требования к синхронизации стека незначительными, поэтому практического ограничения нет.