代码之家  ›  专栏  ›  技术社区  ›  Sergey Mikhanov

局部变量的同步

  •  28
  • Sergey Mikhanov  · 技术社区  · 16 年前

    我有一个多线程的Java代码,其中:

    • 多个线程从同步共享存储中读取有状态对象(即,因此,某些线程可能引用相同的对象);
    • 然后每个线程调用一个方法 process() 把它的物体传给那里;
    • 进程() 以某种方式处理对象,这可能导致更改对象状态;
    • 应同步这些状态更改。

    我创建了这样的方法:

    public void process(Foo[] foos) {
        for (final Foo foo : foos) {
            if (foo.needsProcessing()) {
                synchronized (foo) {
                    foo.process();  // foo's state may be changed here
                }
            }
        }
    }
    

    据我所知,这看起来是合法的。但是,Intellij的检查抱怨在本地变量上进行同步,因为“不同的线程很可能具有不同的本地实例”(这是 对我有效,因为我 在方法中初始化foos)。

    本质上,我想在这里实现的是与方法foo.process()同步相同(这对我来说不是一个选项,因为foo是第三方库的一部分)。

    我习惯了没有黄色标记的代码,所以感谢社区的任何建议。在本地执行同步真的有那么糟糕吗?在我的情况下,还有别的办法吗?

    事先谢谢!

    7 回复  |  直到 8 年前
        1
  •  21
  •   Stephen C    12 年前
    if (foo.needsProcessing()) {
        synchronized (foo) {
            foo.process();  // foo's state may be changed here
        }
    }
    

    我认为在上面的片段中有一个种族条件可能导致 foo.process() 偶尔在同一对象上被调用两次。应该是:

    synchronized (foo) {
        if (foo.needsProcessing()) {
            foo.process();  // foo's state may be changed here
        }
    }
    

    在本地同步真的那么糟糕吗?

    在本地同步也不错 本身 . 真正的问题是:

    • 不同线程是否在正确的对象上同步以实现正确的同步,以及

    • 在这些对象上进行同步是否会导致其他问题。

        2
  •  4
  •   Tatarize    8 年前

    Stephen C的答案有问题,他毫无意义地输入了许多同步锁,奇怪的是,格式化它的更好方法是:

        public void process(Foo[] foos) {
            for (final Foo foo : foos) {
                if (foo.needsProcessing()) {
                    synchronized (foo) {
                        if (foo.needsProcessing()) {
                            foo.process();  // foo's state may be changed here
                        }
                    }
                }
            }
        }
    

    获取同步锁有时可能需要一段时间,如果它被持有,就会发生一些变化。这可能改变了当时FOO的需要处理状态。

    如果不需要处理对象,则不希望等待锁。在获得锁之后,它可能仍然不需要处理。因此,尽管看起来有点愚蠢,新手程序员可能倾向于取消其中一项检查,但只要foo.needsprocessing()是一个可忽略的函数,这实际上是一种很好的方法。


    回到主要问题,当您希望基于数组中的本地值进行同步时,因为时间是关键的。在这些情况下,您最不想做的就是锁定数组中的每一项,或者处理数据两次。只有当有几个线程在做大量工作时,您才会同步本地对象,并且应该很少需要接触相同的数据,但可能非常好。

    只有当且仅当处理需要处理的foo会导致并发错误时,才能执行此操作。在这些情况下,当您只想基于数组中的精确对象进行同步时,基本上需要使用双选通语法。这样可以防止对foo进行双重处理并锁定不需要处理的foo。

    您只剩下很少的情况来阻塞线程,甚至只在重要的时候输入锁,而您的线程只在不阻塞会导致并发错误的地方被阻塞。

        3
  •  2
  •   irreputable    16 年前

    IDE应该帮助你,如果它出错了,你不应该弯腰去取悦它。

    您可以在Intellij中禁用此检查。(有趣的是,它是默认情况下在“线程问题”下启用的唯一一个。这是人们犯的最普遍的错误吗?)

        4
  •  1
  •   David Schmitt    16 年前

    将循环体重构为单独的方法 foo 作为参数。

        5
  •  1
  •   Joonas Pulakka    16 年前

    没有一个自动智能是完美的。我认为您对局部变量进行同步的想法是非常正确的。所以也许你可以按自己的方式来做(这是正确的),并建议JetBrains调整他们的检查。

        6
  •  0
  •   rsp    16 年前

    在集合中的对象上进行同步没有任何问题。您可以尝试用普通for循环替换foreach:

    for (int n = 0; n < Foos.length; n++) {
        Foo foo = Foos[n];
    
        if (null != foo && foo.needsProcessing()) {
            synchronized (foo) {
                foo.process();  // foo's state may be changed here
            }
        }
    }
    

    甚至(这样探测器就不会在 Foo foo ):

    for (int n = 0; n < foos.length; n++) {
        if (null != foos[n] && foos[n].needsProcessing()) {
            synchronized (foos[n]) {
                foos[n].process();  // foos[n]'s state may be changed here
            }
        }
    }
    

    不使用临时值来阻止 foos[n] 这不是最佳实践,但如果它能有效地防止不必要的警告,你可能会接受它。添加一条注释,说明此代码为何具有偏离形式。-)

        7
  •  0
  •   David Schmitt    16 年前

    在.NET世界中,对象有时将其锁定对象作为属性。

    synchronized (foo.getSyncRoot()) {
        if (foo.needsProcessing()) {
            foo.process();  // foo's state may be changed here
        }
    }
    

    这允许对象根据其实现发出不同的锁对象(例如,将监视器委托给基础数据库连接或其他对象)。