为什么在方法参数上同步是“危险的”

2022-09-02 13:38:27

我的IDE(JetBrains IntelliJ IDEA)警告我不要在方法参数上进行同步,即使它始终是一个对象。

enter image description here

完整的警告内容如下:

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

我的猜测是,使用自动装箱,参数可能是转换为对象的基元?虽然,使用自动装箱,我会假设它始终是一个对象,但可能不是共享对象,这意味着它不会是共享同步。

有谁知道为什么会出现警告?在我的情况下,类型始终是一个对象,IDE应该能够知道这一点。ShortCircuit


答案 1

问题是,如果在代码的其他位置使用它时忘记同步,则可能会得到不可预测的结果。在类内部进行同步要好得多,这样可以保证它是线程安全的。ShortCircuitShortCircuit

更新

如果要将同步移动到类之外,则线程处理本质上是不安全的。如果要在外部同步它,则必须审核使用它的所有位置,这就是您收到警告的原因。这一切都与良好的封装有关。如果它是在公共API中,情况会更糟。

现在,如果将方法移动到类中,则可以保证回调不会同时触发。否则,在调用该类上的方法时,需要牢记这一点。fireFinalCallbackShortCircuit


答案 2

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

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

也就是说:首先检查是否返回,如果是这种情况,则执行某些操作(隐藏),这可能会导致状态切换到 。isFinalCallbackFiredfalseisFinalCallbackFiredtrue

因此,我的假设是,粗略地说,将语句放入块中的目的是确保它始终被调用一次ifsynchronizeddoFire


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

可以保证什么:

当两个线程使用相同的参数执行方法时,块将保证一次只有一个线程可以检查状态并(如果是)调用该方法。因此,可以保证只会调用一次fireFinalCallbackShortCircuitsynchronizedisFinalCallbackFiredfalsedoFiredoFire

不能保证的:

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

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

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

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


推荐