Как мы знаем, 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
}
}
}
}
Может ли кто-нибудь помочь написать код для теста синхронизированного реентерабельного предела?
🤔 А знаете ли вы, что...
Java позволяет создавать графические интерфейсы пользователя с помощью библиотеки Swing.
Не прямой ответ, но поскольку единственный способ получить много повторных входов в блоки synchronized
на одном мониторе (или даже на разных мониторах в этом отношении) - это рекурсивные вызовы методов (например, вы не можете программно заблокировать его в узком цикле) у вас закончится пространство стека вызовов, прежде чем вы достигнете предела счетчика, который JVM внутренне хранит для этого.
Why does a thread support just 2,147,483,647 is something I am curious to know as well now!
Ну, во-первых, этого достаточно... Но это будет реализовано с помощью счетчика повторных входов, а эти штуки со временем переполняются.
Поскольку спецификация не определяет предел, она зависит от реализации. Там даже не обязательно должно быть ограничение, но JVM часто оптимизируются для высокой производительности, учитывая обычные варианты использования, а не сосредотачиваясь на поддержке крайних случаев.
Как сказано в этот ответ, существует фундаментальная разница между встроенным монитором объекта и ReentrantLock
, поскольку вы можете получить последний в цикле, что делает необходимым указать, что существует ограничение.
Проблема определения фактического предела конкретной реализации JVM, такой как широко используемая JVM HotSpot, заключается в том, что существует несколько факторов, которые могут повлиять на результат даже в одной и той же среде.
Чтобы поэкспериментировать с фактической реализацией, я использовал библиотеку КАК М для генерации байт-кода, который получает монитор объекта в цикле, действие, которое обычный код 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 байтами, а для 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
после большого количества вызовов, которое меняется от запуска к запуску и зависит от наличия таких параметров, как -Xcomp
или -Xint
, указывая на то, что он подлежит к упомянутому выше недетерминированному размеру стека.
Но когда вы значительно увеличиваете количество вложенных блоков synchronized
, количество вложенных вызовов становится меньше и стабильнее. В моей среде он произвел StackOverflowError
после 30 вложенных вызовов при наличии 1000 вложенных блоков synchronized
и 15 вложенных вызовов при наличии 2000 вложенных блоков synchronized
, что довольно последовательно, указывая на то, что накладные расходы на вызов метода стали неактуальными.
Это подразумевает более 30 000 сборов данных, что примерно вдвое меньше, чем с помощью сгенерированного кода ASM, что разумно, учитывая, что сгенерированный код обеспечит соответствующее количество сборов и выпусков, вводя синтетическую локальную переменную, содержащую ссылку на объект, который должен быть выпущен для каждого javac
блока. Эта дополнительная переменная уменьшает доступный размер стека. Это также причина, по которой мы теперь видим synchronized
, а не StackOverflowError
, так как этот код правильно делает структурированная блокировка.
Как и в другом примере, работа с большим размером стека увеличивает сообщаемое число, линейно масштабируясь. Экстраполяция результатов подразумевает, что для захвата монитора IllegalMonitorStateException
раз потребуется стек размером в несколько ГБ. Присутствует ли ограничивающий счетчик или нет, в этих обстоятельствах становится неважным.
Конечно, эти примеры кода настолько далеки от кода реального приложения, что неудивительно, что здесь не произошло много оптимизаций. Для кода реального приложения устранение блокировок и их огрубление могут происходить с гораздо большей вероятностью. Кроме того, реальный код будет выполнять фактические операции, требующие пространства стека, самостоятельно, что делает требования к стеку для синхронизации незначительными, поэтому практических ограничений нет.