Java 在线程之间共享容器数组

Java 在线程之间共享容器数组,java,multithreading,Java,Multithreading,我有一个应用程序,是多线程和工作正常。然而,它遇到了锁争用问题(通过抓拍java堆栈并查看等待的内容来检查) 每个线程使用列表中的对象,并拒绝每个对象或将其放入一个容器中 垃圾箱最初是空的,因为每个垃圾箱都可能很贵(并且可能有很多垃圾箱) 导致争用的代码大致如下所示: public void addToBin(Bin[] bins, Item item) { Bin bin; int bin_index = item.bin_index synchronized(bins) {

我有一个应用程序,是多线程和工作正常。然而,它遇到了锁争用问题(通过抓拍java堆栈并查看等待的内容来检查)

每个线程使用列表中的对象,并拒绝每个对象或将其放入一个容器中

垃圾箱最初是空的,因为每个垃圾箱都可能很贵(并且可能有很多垃圾箱)

导致争用的代码大致如下所示:

public void addToBin(Bin[] bins, Item item) {
   Bin bin;
   int bin_index = item.bin_index
   synchronized(bins) {
      bin = bins[bin_index];
      if(bin==null) {
        bin = new Bin();
        bins[bin_index] = bin;
      }
   }
   synchronized(bin) {
     bin.add(item);
   }
}
public void addToBin(Bin[] bins, Item item) {
   int bin_index = item.bin_index
   Bin bin = bins[bin_index];

   if(bin==null) {
     synchronized(bins) {
        bin = bins[bin_index];
        if(bin==null) {
          bin = new Bin();
          bins[bin_index] = bin;
        }
     }
   }

   synchronized(bin) {
     bin.add(item);
   }
}
存储箱
阵列上的同步是瓶颈

一位同事建议我使用双重检查锁定来解决这个问题,但我们不确定到底需要什么来确保安全。建议的解决方案如下所示:

public void addToBin(Bin[] bins, Item item) {
   Bin bin;
   int bin_index = item.bin_index
   synchronized(bins) {
      bin = bins[bin_index];
      if(bin==null) {
        bin = new Bin();
        bins[bin_index] = bin;
      }
   }
   synchronized(bin) {
     bin.add(item);
   }
}
public void addToBin(Bin[] bins, Item item) {
   int bin_index = item.bin_index
   Bin bin = bins[bin_index];

   if(bin==null) {
     synchronized(bins) {
        bin = bins[bin_index];
        if(bin==null) {
          bin = new Bin();
          bins[bin_index] = bin;
        }
     }
   }

   synchronized(bin) {
     bin.add(item);
   }
}

这是否安全和/或是否有更好/更安全/更惯用的方法来做到这一点?

我建议不要使用第二种解决方案,因为它访问同步块外部的
bin
数组,因此不能保证另一个线程所做的更改对正在从中读取未同步元素的代码可见


不能保证会看到同时添加的新
Bin
,因此它可能会再次为同一索引创建一个新的
Bin
,并丢弃一个同时创建和存储的Bin,同时也忘记了项目可能会放在丢弃的Bin中。

Java有各种优秀的无锁并发数据结构,因此对于这种类型的事情,实际上不需要使用具有同步的数组

是一个并发、排序的键值映射。 是一个并发的未排序键值

您可以简单地使用其中一个而不是数组。只需将map键设置为已经使用的整数索引,就可以了


还有谷歌和谷歌,它们在保持有序数据和删除旧条目方面非常出色。

正如Malt的答案中所述,Java已经提供了许多无锁数据结构和概念,可用于解决此问题。我想使用
AtomicReferenceArray
添加一个更详细的示例:

假设,
bins
是一个
AtomicReferenceArray
,以下代码在出现
null
条目时执行无锁更新:

Bin bin = bins.get(index);
while (bin == null) {
    bin = new Bin();
    if (!bins.compareAndSet(index, null, bin)) {
        // some other thread already set the bin in the meantime
        bin = bins.get(index);
    }
}
// use bin as usual
自Java 8以来,有一种更优雅的解决方案:

Bin bin = bins.updateAndGet(index, oldBin -> oldBin == null ? new Bin() : oldBin);
// use bin as usual

Node:Java 8版本(尽管仍然是非阻塞的)明显比上面的Java 7版本慢,因为
updateAndGet
将始终更新数组,即使值没有更改。根据整个bin更新操作的总体成本,这可能可以忽略,也可能不可以忽略


另一个非常优雅的策略可能是在将数组移交给工作线程之前,用新创建的
Bin
实例预先填充整个
Bin
数组。由于线程不必修改数组,因此这将减少同步到
Bin
对象本身的需要。通过使用
数组。parallelSetAll
(自Java 8以来),可以轻松地多线程填充数组:

更新2:如果这是一个选项,则取决于算法的预期输出:最后
存储箱会被完全、密集还是稀疏地填充?(在第一种情况下,预填充是可取的。在第二种情况下,这取决于-通常如此。在后一种情况下,这可能是一个坏主意)



更新1:不要使用双重检查锁定!这不安全!这里的问题是可见性,而不是原子性。在您的情况下,读取线程可能会得到一个部分构造(因此损坏)的
Bin
实例。有关详细信息,请参见。

如果没有内置java类对您有所帮助,您可以只创建8个bins锁,比如binsALock to binsFLock

然后将bin_索引除以8,使用提醒选择要使用的锁


如果您选择了一个大于现有线程数的较大数字,并且在争用时使用了一个非常快的锁,那么您可能会比选择8做得更好


通过减少使用的线程数,也可以获得更好的结果。

我不这么认为。在第二种解决方案中,再次检查同步区域中的bin是否保持为空。因此,可以保证只添加一个bin。至少有一个执行路径在阵列上没有正确同步,因此可能存在一个微妙但被忽略的可见性问题,使其失败。因此,建议不要使用双重检查锁定是一个很好的建议,特别是当有其他更优雅的方法来解决这个问题时。@isnot2bad什么执行路径?我愿意学习,不会看到有问题的路径。@如果从数组中检索到的bin不为null,则代码路径不会进入数组上的块同步,因此读取线程可能不会看到完全构造的bin实例。有关详细信息,请参阅。这是一个可见性问题,而不是原子性问题。为什么不使用一个队列来为一个管理垃圾箱的收集器线程提供数据呢?另外,更具体地说,“昂贵”和“很多”。除非我们讨论的是本机数据结构或反射以及成百上千的数据,否则这种惰性初始化看起来像是过早的优化。(假设您的binning值多线程的开销,我假设您的输入数据集很大。)数百万个bins并不罕见。数据集可以是10 TB。内存使用率是10s-100s GB。那么,为什么这些都保存在RAM中呢?这听起来像是这样的工作,在每个存储箱中附加一个文件会更有效。这一切都会被保存在内存中,因为命中磁盘的速度很慢。它已经尽可能少地从磁盘上提取对象,将中间结果写回磁盘将降低性能(比现有的锁争用更糟糕)。一旦数据被装箱和处理,最终的简化结果将被存入磁盘。不要忘记