使用并发哈希映射时锁的必要性

Necessity of the locks while working with concurrent hash map

这是我的 classes 之一中的代码:

 class SomeClass {

   private Map<Integer, Integer> map = new ConcurrentHashMap<>();
   private volatile int counter = 0;
   final AtomicInteger sum = new AtomicInteger(0); // will be used in other classes/threads too
   private ReentrantLock l = new ReentrantLock();

   public void put(String some) {
    l.lock();
    try {
        int tmp = Integer.parseInt(some);
        map.put(counter++, tmp);
        sum.getAndAdd(tmp);
    } finally {
        l.unlock();
    }
   }

   public Double get() {
    l.lock();
    try {
        //... perform some map resizing operation ...
        // some calculations including sum field ...
    } finally {
        l.unlock();
    }
   }

}

您可以假定此 class 将在并发环境中使用。

问题是:您认为锁具的必要性如何?这段代码闻起来怎么样? :)

因为当您将它用作放入此地图的键时,您总是递增 counter

map.put(counter++, tmp);

当你再次阅读时:

return sum / map.get(counter);

map.get(counter) 将是 null,因此这会导致 NPE(除非您将超过 2^32 的东西放入地图中,ofc)。 (我假设你的意思是 sum.get(),否则它不会编译)。

因此,您可以在没有任何锁的情况下拥有等效的功能:

class SomeClass {
  public void put(String some) { /* do nothing */ }
  public Double get() {
    throw new NullPointerException();
  }
}

您还没有真正解决 edit 的问题。 divisor 仍然为空,因此没有锁的等效功能为:

class SomeClass {
  private final AtomicInteger sum = new AtomicInteger(0);

  public void put(String some) {
    sum.getAndAdd(Integer.parseInt(some));
  }

  public Double get() {
    return sum.get();
  }
}

再看看public void put(String some)里面的操作。

  1. map.put(counter++, tmp);
  2. sum.getAndAdd(tmp);

现在让我们看看各个部分。

  1. counter 是一个可变变量。所以它只提供内存可见性而不提供原子性。由于counter++是复合操作,所以需要加锁实现原子性

  2. map.put(key, value) 是原子的,因为它是 ConcurrentHashMap.

  3. sum.getAndAdd(tmp) 是原子的,因为它是 AtomicInteger.

如您所见,除了 counter++ 之外,其他所有操作都是原子操作。但是,您正试图通过组合所有这些操作来实现某些功能。要在功能级别实现原子性,您需要一个锁。当线程在各个原子操作之间交错时,这将帮助您避免意外的副作用。

所以你需要一个锁,因为 counter++ 不是原子的,你想结合一些原子操作来实现一些功能(假设你希望它是原子的).