锁定或等待缓存加载

时间:2014-08-11 14:12:00

标签: java multithreading concurrency locking

我们需要锁定一个负责将数据库日期加载到基于HashMap的缓存中的方法。

可能的情况是第二个线程在第一个方法仍在加载缓存时尝试访问该方法。

我们认为在这种情况下第二个线程的努力是多余的。因此,我们希望第二个线程等到第一个线程完成,然后返回(不再加载缓存)。

我的作品,但似乎相当不雅。有更好的解决方案吗?

private static final ReentrantLock cacheLock = new ReentrantLock();
private void loadCachemap() {
    if (cacheLock.tryLock()) {
        try {
            this.cachemap = retrieveParamCacheMap();
        } finally {
            cacheLock.unlock();
        }
    } else {
        try {           
            cacheLock.lock(); // wait until thread doing the load is finished
        } finally {
            try {
                cacheLock.unlock();
            } catch (IllegalMonitorStateException e) {
                logger.error("loadCachemap() finally {}",e);
            }
        }
    }
}

3 个答案:

答案 0 :(得分:4)

我更喜欢使用读锁和写锁的更有弹性的方法。类似的东西:

private static final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
private static final Lock cacheReadLock = cacheLock.readLock();
private static final Lock cacheWriteLock = cacheLock.writeLock();

private void loadCache() throws Exception {
    // Expiry.
    while (storeCache.expired(CachePill)) {
        /**
         * Allow only one in - all others will wait for 5 seconds before checking again.
         *
         * Eventually the one that got in will finish loading, refresh the Cache pill and let all the waiting ones out.
         *
         * Also waits until all read locks have been released - not sure if that might cause problems under busy conditions.
         */
        if (cacheWriteLock.tryLock(5, TimeUnit.SECONDS)) {
            try {
                // Got a lock! Start the rebuild if still out of date.
                if (storeCache.expired(CachePill)) {
                    rebuildCache();
                }
            } finally {
                cacheWriteLock.unlock();
            }
        }
    }
}

请注意,storeCache.expired(CachePill)检测到的陈旧缓存可能比您想要的更多,但这里的概念是相同的,在更新缓存之前建立写锁定,这将拒绝所有读取尝试,直到重建完成。此外,在某种循环中管理多次写入尝试,或者只是退出并让读锁等待访问。

现在,缓存中的读取内容如下所示:

public Object load(String id) throws Exception {
    Store store = null;
    // Make sure cache is fresh.
    loadCache();
    try {
        // Establish a read lock so we do not attempt a read while teh cache is being updated.
        cacheReadLock.lock();
        store = storeCache.get(storeId);
    } finally {
        // Make sure the lock is cleared.
        cacheReadLock.unlock();
    }
    return store;
}

此表单的主要好处是读访问不会阻止其他读访问,但在重建期间一切都会完全停止 - 甚至是其他重建。

答案 1 :(得分:0)

您没有说您的结构有多复杂以及您需要多少并发/拥塞。有很多方法可以满足您的需求。

如果您的数据很简单,请使用ConcurrentHashMap或类似数据来保存数据。然后无论如何只需读取和写入线程。

另一种方法是使用actor模型并将读/写放在同一队列中。

答案 2 :(得分:-1)

如果你需要的只是填写一个只读的映射,一旦请求就从数据库初始化,你可以使用任何形式的双重检查锁定,这可以通过多种方式实现。最简单的变体如下:

  private volatile Map<T, V> cacheMap;

  public void loadCacheMap() {
      if (cacheMap == null) {
          synchronized (this) {
              if (cacheMap == null) {
                  cacheMap = retrieveParamCacheMap();
              }
          }
      }
  }

但是我个人更喜欢在这里避免任何形式的同步,只是确保在任何其他线程可以访问它之前完成初始化(例如在DI容器中以init方法的形式)。在这种情况下,您甚至可以避免使用volatile的开销。

编辑:答案仅在预期初始加载时有效。如果有多个更新,您可以尝试通过其他形式的测试和测试和设置替换tryLock,例如使用以下内容:

  private final AtomicReference<CountDownLatch> sync = 
    new AtomicReference<>(new CountDownLatch(0));

  private void loadCacheMap() {
      CountDownLatch oldSync = sync.get();
      if (oldSync.getCount() == 0) { // if nobody updating now
          CountDownLatch newSync = new CountDownLatch(1);
          if (sync.compareAndSet(oldSync, newSync)) {
              cacheMap = retrieveParamCacheMap();
              newSync.countDown();
              return;
          }
      }
      sync.get().await();
  }