【问题标题】:Synchronizing elements in an array同步数组中的元素
【发布时间】:2012-09-06 16:38:57
【问题描述】:

我是 Java 多线程的新手,不太了解发生了什么。

从在线教程和讲义中,我知道必须将synchronized 块应用于非空对象,以确保只有一个线程可以执行该代码块。由于数组是 Java 中的对象,因此可以对其应用同步。此外,如果数组存储对象,我也应该能够同步数组的每个元素。

我的程序有几个线程更新了一个数字数组,因此我创建了一个 Long 对象数组:

synchronized (grid[arrayIndex]){
    grid[arrayIndex] += a.getNumber();
}

这段代码位于我扩展的线程类的run() 方法中。数组 grid 由我的所有线程共享。但是,在一个线程上运行相同的程序时,这不会返回正确的结果。

【问题讨论】:

    标签: java arrays multithreading wrapper thread-synchronization


    【解决方案1】:

    这行不通。重要的是要意识到grid[arrayIndex] += ... 实际上是用新对象替换grid 中的元素。这意味着您正在对数组中的一个对象进行同步,然后立即用数组中的另一个对象替换该对象。这将导致其他线程锁定不同的对象,因此它们不会阻塞。您必须锁定一个常量对象。

    您可以改为锁定整个数组对象,如果它永远不会被另一个数组对象替换:

    synchronized (grid) {
        // this changes the object to another Long so can't be used to lock
        grid[arrayIndex] += a.getNumber();
    }
    

    这就是为什么锁定final 对象是一种很好的模式的原因之一。请参阅此答案以了解更多详细信息:

    Why is it not a good practice to synchronize on Boolean?

    【讨论】:

    • 你是对的,但锁定整个数组可能有点矫枉过正。您可以使用锁定对象设置兄弟数组 final Object[] locks = new Object[arrayLength]; for (int i = 0; i
    • @ashirley 值得注意的是,数组永远不应该修改其元素。它应该被初始化、填充,然后再也不会改变。不幸的是,将其声明为 final 并不足以保证其内部元素不会改变。
    • OP 仅保护数组分配@ashirley。在这种情况下,锁定粒度无关紧要。
    • @Brian 是的,总比没有好
    • @Gray 是的,但值得指出的是,它会演变成更冗长的操作
    【解决方案2】:

    另一种选择是使用AtomicLong 对象的数组,并使用它们的addAndGet()getAndAdd() 方法。您不需要同步来增加对象,并且可以同时增加多个对象。

    【讨论】:

    • 好一个@JB。当然,你还是要为内存屏障付出代价。
    【解决方案3】:

    java 类 Long 是不可变的,你不能改变它的值。所以当你执行一个动作时:

    grid[arrayIndex] += a.getNumber();
    

    它并没有改变你正在锁定的 grid[arrayIndex] 的值,而是实际上创建了一个新的 Long 对象并将其值设置为旧值加上 a.getNumber。所以你最终会在不同的对象上同步不同的线程,这会导致你看到的结果

    【讨论】:

      【解决方案4】:

      你在这里的synchronized 块不好。当您在数组元素(可能是一个数字)上进行同步时,您只在该对象上进行同步。当您将数组的元素重新分配给与开始时不同的对象时,同步不再在正确的对象上,其他线程将能够访问该索引。

      这两个选项之一会更正确:

      private final int[] grid = new int[10];
      
      synchronized (grid) {
          grid[arrayIndex] += a.getNumber();
      }
      

      如果grid不能是final

      private final Object MUTEX = new Object();
      
      synchronized (MUTEX) {
          grid[arrayIndex] += a.getNumber();
      }
      

      如果您使用第二个选项并且grid 不是final,则还应同步对grid 的任何分配。

      synchronized (MUTEX) {
          grid = new int[20];
      }
      

      总是在最终的东西上同步,总是在访问和修改上同步,一旦你完成了,你就可以开始研究其他锁定机制,例如LockReadWriteLockSemaphore。这些可以提供比同步更复杂的锁定机制,这对于仅 Java 的默认同步还不够的情况更好,例如在高吞吐量系统中锁定数据 (read/write locking) 或锁定在资源池中 (counting semaphores)。

      【讨论】:

      • 我不同意最后一段。这些锁需要更仔细的编程(尝试/最终),因此不能称为“更安全”或“更可预测”,也不能“避免很多问题”。反之亦然。更强大,在某些情况下性能更好,是的。
      • 同样在第一个,你不同步一个数字,你同步一个对象。您还使用了具有误导性的单词值。
      • @Gray 我会承认安全这个词并将其删除。只要您遵循任何同步的正确模式,它就会是安全的。至于可预测的,本机 Java 同步不可预测的。如果有两个或更多线程正在等待它,则接收监视器的线程是随机的。对于其他系统,我可以打开公平性并使其排队线程并提供锁先到先服务而不是随机顺序,从而防止线程饥饿等事情。根据您的第二条评论,我同意,措辞已更改。
      • 有趣的 RE:可预测性,但我怀疑这是语言定义 @Brian 的学术问题。你知道在实践中这是真的任何情况吗?由于这个原因,我认为使用Lock 而不是synchronized 是非常危险的。 特别是当你在 SO 上为新手线程程序员提供咨询时。
      • @Gray 据我了解,调度程序唤醒的第一个在监视器上阻塞的线程是得到它的线程,所以在实践中,如果处理器调度程序的算法唤醒,这是可能的线程的顺序总是将其中一个线程放在其他线程后面。我想从更实际的意义上说,大多数系统(Windows 和 *NIX)都有很好的调度算法,可能会规避更严重的饥饿问题。但是,在使用您希望为 FIFO 提供锁的系统(例如在时间敏感的请求/响应系统中)工作时,最好有保证。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-02-02
      • 1970-01-01
      • 2015-04-20
      • 2021-09-21
      • 2014-08-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多