Почему синхронизация по параметру метода «опасна» - PullRequest
0 голосов
/ 20 февраля 2019

Моя IDE (JetBrains IntelliJ IDEA) предупреждает меня о синхронизации с параметром метода, даже если это всегда объект.

enter image description here

Полное предупреждение гласит:

Синхронизация по параметру метода 's' ... Информация о проверке: Сообщает о синхронизации по локальной переменной или параметру.При такой синхронизации очень трудно гарантировать правильность.Может быть возможно улучшить подобный код, управляя доступом через, например, синхронизированный класс-обертку, или синхронизируя по полю.

Я предполагаю, что с автобоксом параметр может быть примитивомкоторый превращается в объект?Хотя, с автоматическим боксом, я бы предположил, что это всегда объект, но, возможно, не общий объект, что означает, что это не будет общая синхронизация.

Кто-нибудь знает, почему будет появляться предупреждение?В моем случае тип ShortCircuit всегда является объектом, и среда IDE должна знать это.

Ответы [ 2 ]

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

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

synchronized (s)
{
    if (!s.isFinalCallbackFired())
    {
        s.doFire();
    }
}

То есть: сначала проверяется, возвращает ли isFinalCallbackFired false, и если этоВ этом случае что-то (скрытое) выполнено, что, вероятно, приводит к переключению состояния isFinalCallbackFired на true.

Итак, я предполагаю, что цель помещения оператора if в блок synchronized состояла в том, чтобы убедиться, что doFire всегда вызывается точно один раз .


И действительно, на этом этапе синхронизация может быть оправдана.Точнее говоря, немного упрощенно:

Что может быть гарантировано :

Когда два потока выполняют метод fireFinalCallback с одним и тем же параметром ShortCircuit,Блок synchronized гарантирует, что только один поток может одновременно проверять состояние isFinalCallbackFired и (если это false) вызывать метод doFire.Таким образом, гарантируется, что doFire будет вызываться только один раз .

Что не может быть гарантировано :

Когда один поток выполняет метод fireFinalCallback, а другой поток выполняет любую операцию над объектом ShortCircuit (например, вызовом)doFire), тогда это может привести к противоречивому состоянию.В частности, если другой поток также выполняет

if (!s.isFinalCallbackFired())
{
    s.doFire();
}

, но без синхронизации на объекте, то doFire может вызываться дважды.


MCVE, который иллюстрирует эффект:

public class SynchronizeOnParameter
{
    public static void main(String[] args)
    {
        System.out.println("Running test without synchronization:");
        runWithoutSync();
        System.out.println();

        System.out.println("Running test with synchronization:");
        runWithSync();
        System.out.println();

        System.out.println("Running test with wrong synchronization:");
        runWithSyncWrong();
        System.out.println();

    }

    private static void runWithoutSync()
    {
        ShortCircuit s = new ShortCircuit();
        new Thread(() -> fireFinalCallbackWithoutSync(s)).start();
        pause(250);
        new Thread(() -> fireFinalCallbackWithoutSync(s)).start();
        pause(1000);
    }

    private static void runWithSync()
    {
        ShortCircuit s = new ShortCircuit();
        new Thread(() -> fireFinalCallbackWithSync(s)).start();
        pause(250);
        new Thread(() -> fireFinalCallbackWithSync(s)).start();
        pause(1000);
    }

    private static void runWithSyncWrong()
    {
        ShortCircuit s = new ShortCircuit();
        new Thread(() -> fireFinalCallbackWithSync(s)).start();

        if (!s.isFinalCallbackFired())
        {
            s.doFire();
        }
    }



    private static void fireFinalCallbackWithoutSync(ShortCircuit s)
    {
        if (!s.isFinalCallbackFired())
        {
            s.doFire();
        }
    }

    private static void fireFinalCallbackWithSync(ShortCircuit s)
    {
        synchronized (s)
        {
            if (!s.isFinalCallbackFired())
            {
                s.doFire();
            }
        }
    }

    static class ShortCircuit
    {
        private boolean fired = false;

        boolean isFinalCallbackFired()
        {
            return fired;
        }

        void doFire()
        {
            System.out.println("Calling doFire");
            pause(500);
            fired = true;
        }
    }

    private static void pause(long ms)
    {
        try
        {
            Thread.sleep(ms);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

}

Выход составляет

Running test without synchronization:
Calling doFire
Calling doFire

Running test with synchronization:
Calling doFire

Running test with wrong synchronization:
Calling doFire
Calling doFire

Так что synchonized block делает , чтобы убедиться, что метод doFireзвонил только один раз.Но это работает, только если все модификации выполняются только в методе fureFinalCallback.Если объект изменяется в другом месте, без блока synchronized, метод doFire можно вызвать дважды.

(Я хотел бы предложить решение для этого, но без подробностей о ShortCircuitclass, а также оставшиеся классы и процессы, можно только дать смутный намек, чтобы взглянуть на пакет java.util.concurrent и его подпакеты: блокировки и условия могут быть жизнеспособным путем, но вы должны понять, чтовне ...)

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

Дело в том, что если вы забудете синхронизироваться на ShortCircuit при использовании его в других местах вашего кода, вы можете получить непредсказуемые результаты.Гораздо лучше синхронизировать внутри класса ShortCircuit, поэтому он гарантированно безопасен для потоков.

Обновление

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

Теперь, если вы переместите метод fireFinalCallback в свой класс ShortCircuit, вы можете гарантировать, что обратный вызов не сработает одновременно.В противном случае вы должны иметь это в виду при вызове методов этого класса.

...