在线程之间共享二进制数组

时间:2014-08-06 06:56:59

标签: java multithreading

我有一个多线程的应用程序,工作正常。然而,它正在达到锁争用问题(通过快照java堆栈并查看最新的等待来检查)。

每个线程都会从列表中删除对象,并拒绝每个对象或将其放入Bin。

Bins最初为null,因为每个Bins都很昂贵(并且可能有很多)。

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

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);
   }
}

bins数组上的同步是瓶颈。

一位同事向我建议使用双重检查锁定来解决这个问题,但我们不确定究竟会涉及到什么使其安全。建议的解决方案如下所示:

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);
   }
}

这样做是否安全和/或有更好/更安全/更惯用的方法吗?

4 个答案:

答案 0 :(得分:6)

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

假设binsAtomicReferenceArray,以下代码在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

节点:Java 8版本 - 虽然仍然没有阻塞 - 明显慢于上面的Java 7版本,因为updateAndGet总是更新数组,即使价值不会改变。这可能会或可能不会忽略,具体取决于整个bin更新操作的总成本。


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

Arrays.parallelSetAll(bins, i -> new Bin());

更新2:如果这是一个选项,取决于算法的预期输出:最终bins数组是完全填充,密集还是稀疏填充? (在第一种情况下,预填充是可取的。在第二种情况下,它取决于 - 经常。在后一种情况下,它可能是一个坏主意。)


更新1:请勿使用双重检查锁定!这不安全!这里的问题是可见性,而不是原子能。在您的情况下,读取线程可能会获得部分构造(因此损坏)Bin实例。有关详细信息,请参阅http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

答案 1 :(得分:5)

Java具有各种优秀的无锁并发数据结构,因此不需要为这类事物使用具有同步的数组。

ConcurrentSkipListMap是一个并发的,有序的键值映射。 ConcurrentHashMap是一个并发的未排序键值。

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

还有Google的ConcurrentLinkedHashMap和Google的Guava Cache,它们非常适合保存有序数据和删除旧条目。

答案 2 :(得分:0)

我建议不要使用第二个解决方案,因为它访问同步块之外的bins数组,因此无法保证另一个线程所做的更改对于从其中读取元素的代码是不可见的。

无法保证会同时添加新的Bin,因此它可能会再次为同一个索引创建一个新Bin并丢弃同时创建和存储的一个 - 也会忘记该项可能放在丢弃的那个。

答案 3 :(得分:0)

如果没有内置的java类可以帮助你,你可以创建8个bin锁,比如binsALock到binsFLock。

然后将bin_index除以8,使用提醒选择要使用的锁。


如果您选择的数字大于您拥有的线程数,并且在争用时使用非常快的锁定,那么您可能比选择8更好。

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