Есть ли у синхронизированного блока максимальное количество повторных входов?

Как мы знаем, 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.


7
464
2

Ответы:

Не прямой ответ, но поскольку единственный способ получить много повторных входов в блоки 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, заключается в том, что существует несколько факторов, которые могут повлиять на результат даже в одной и той же среде.

  • JVM может устранять блокировки, когда она может доказать, что объект является чисто локальным, т.е. невозможно, чтобы другой поток когда-либо синхронизировался с ним.
  • JVM может объединять соседние и вложенные синхронизированные блоки, когда они используют один и тот же объект, что может применяться после встраивания, поэтому эти блоки не должны отображаться вложенными или близкими друг к другу в исходном коде.
  • JVM может иметь разные реализации, выбранные на основе формы класса объекта (некоторые классы с большей вероятностью будут использоваться в качестве ключа синхронизации) и истории конкретного приобретения (например, использовать предвзятую блокировку или использовать оптимистичные или пессимистические подходы, в зависимости от от того, как часто блокировка оспаривалась)

Чтобы поэкспериментировать с фактической реализацией, я использовал библиотеку КАК М для генерации байт-кода, который получает монитор объекта в цикле, действие, которое обычный код 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 раз потребуется стек размером в несколько ГБ. Присутствует ли ограничивающий счетчик или нет, в этих обстоятельствах становится неважным.

Конечно, эти примеры кода настолько далеки от кода реального приложения, что неудивительно, что здесь не произошло много оптимизаций. Для кода реального приложения устранение блокировок и их огрубление могут происходить с гораздо большей вероятностью. Кроме того, реальный код будет выполнять фактические операции, требующие пространства стека, самостоятельно, что делает требования к стеку для синхронизации незначительными, поэтому практических ограничений нет.