我有一个应用程序是多线程和工作正常.然而,它遇到了锁争用问题(通过快照查看
java堆栈并查看等待的内容).
每个线程都从列表中消除对象,并拒绝每个线程或将其放入Bin.
箱子最初是空的,因为每个都可以是昂贵的(并且有可能很多它们).
导致争用的代码大致如下:
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); } }
这是安全的还是/或者有更好/更安全/更惯用的方式来做到这一点吗?
解决方法
如Malt的答案所述,Java已经提供了许多无锁数据结构和概念,可以用来解决这个问题.我想使用AtomicReferenceArray添加更详细的示例:
假设bins是一个AtomicReferenceArray,以下代码执行一个空闲的空闲更新,以防空条目:
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
节点:由于事实上,即使值不改变,updateAndGet将始终更新数组,Java 8版本仍然是非阻塞状态.这可能或可能不可忽略,取决于整个bin更新操作的总体成本.
另一个非常优雅的策略可能是将新建的Bin实例的整个bin数组预先填满,然后将数组移交给工作线程.由于线程不必修改阵列,这将减少与Bin对象本身同步的需要.通过使用Arrays.parallelSetAll(从Java 8开始)可以轻松完成数组填充多线程:
Arrays.parallelSetAll(bins,i -> new Bin());
更新2:如果这是一个选项取决于您的算法的预期输出:最终将数组填充完整,密集或只是稀疏? (在第一种情况下,预先填充是可取的,在第二种情况下,它依赖于这种情况,在后一种情况下,这可能是一个坏主意).
更新1:不要使用双重检查锁定!这不安全!这里的问题是可见性,而不是atomicitiy.在您的情况下,读取线程可能会部分构造(因此损坏)Bin实例.详见http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.