Синхронизированный блок имеет максимальный предел повторного входа? - PullRequest
0 голосов
/ 27 февраля 2019

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

Может кто-нибудь помочь написатькакой-нибудь код для синхронизированного теста предела повторного входа?

Ответы [ 2 ]

0 голосов
/ 27 февраля 2019

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

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

0 голосов
/ 27 февраля 2019

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

Почему поток поддерживает только 2 147 483 647, мне интересночтобы знать это сейчас!

Ну, во-первых, этого достаточно ... Но это будет реализовано с помощью счетчика повторного входа, и эти вещи со временем будут переполнены.

...