为什么同步方法参数是"危险的"

Ale*_*lls 12 java jetbrains-ide intellij-idea

我的IDE(JetBrains IntelliJ IDEA)警告我关于方法参数的同步,即使它始终是一个对象.

在此输入图像描述

完整警告如下:

方法参数''的同步...检查信息:报告局部变量或参数的同步.使用这种同步时很难保证正确性.通过例如同步包装类控制访问或通过在字段上同步来改进这样的代码是可能的.

我的猜测是,使用自动装箱,参数可能是一个转换为对象的原语?虽然,使用自动装箱,我会认为它始终是一个对象,但也许不是共享对象,这意味着它不会是共享同步.

任何人都知道警告会出现的原因吗?在我的情况下,ShortCircuit类型始终是一个对象,IDE应该能够知道.

jon*_*tro 8

问题是,如果您ShortCircuit在代码的其他位置使用它时忘记了同步,则可能会得到不可预测的结果.在ShortCircuit类内部同步更好,所以它保证是线程安全的.

更新

如果您将同步移到类之外,那么它对于线程来说本质上是不安全的.如果你想在外部同步它,你将必须审核它使用的所有地方,这就是你得到警告的原因.这都是关于良好的封装.如果是在公共API中,情况会更糟.

现在,如果将fireFinalCallback方法移动到您的ShortCircuit类,您可以保证回调不会同时触发.否则,在调用该类的方法时需要记住这一点.


Mar*_*o13 6

正如jontro 在他的回答中已经提到的(基本上,正如警告已经说过的那样):ShortCircuit对象上的这种同步不具有开发人员可能希望实现的效果。不幸的是,屏幕截图中的工具提示隐藏了实际代码,但代码似乎大致是

synchronized (s)
{
    if (!s.isFinalCallbackFired())
    {
        s.doFire();
    }
}
Run Code Online (Sandbox Code Playgroud)

即:首先检查是否isFinalCallbackFired返回false,如果是这种情况,则执行某些(隐藏的)操作,这可能会导致isFinalCallbackFired状态切换到true

所以我的假设是,粗略地说,将if语句放入synchronized块中的目的是确保doFire总是调用恰好一次


事实上,在这一点上,同步是合理的。更具体地说,有点过于简化:

什么可以保证:

当两个线程fireFinalCallback使用相同的ShortCircuit参数执行方法时,synchronized块将保证一次只有一个线程可以检查isFinalCallbackFired状态并(如果是false)调用该doFire方法。所以保证doFire只会被调用一次

什么不能保证:

当一个线程正在执行该fireFinalCallback方法,而另一个线程对该ShortCircuit对象执行任何操作(如调用doFire)时,这可能会导致不一致的状态。特别是,如果另一个线程也这样做

if (!s.isFinalCallbackFired())
{
    s.doFire();
}
Run Code Online (Sandbox Code Playgroud)

没有同步对象,则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();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

输出是

Running test without synchronization:
Calling doFire
Calling doFire

Running test with synchronization:
Calling doFire

Running test with wrong synchronization:
Calling doFire
Calling doFire
Run Code Online (Sandbox Code Playgroud)

因此该synchonized确实确保该doFire方法仅被调用一次。但这只有在所有修改都只在fureFinalCallback方法中完成时才有效。如果对象在其他地方被修改,没有synchronized块,该doFire方法可能会被调用两次。

(我想为此提供一个解决方案,但没有关于ShortCircuit类和其余类和进程的详细信息,人们只能给出一个模糊的提示来查看java.util.concurrent包及其子包:Locks and conditions 可能是一个可行的路径,但你必须弄清楚......)