【问题标题】:Lock or wait cache load锁定或等待缓存加载
【发布时间】:2014-08-11 14:12:00
【问题描述】:

我们需要锁定一个负责将数据库日期加载到基于 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);
            }
        }
    }
}

【问题讨论】:

  • 您的流程将只确保retrieveParamCacheMap() 在此流程中不会被调用,即从loadCachemap()。任何其他线程都可以从不同的地方调用您的retrieveParamCacheMap()。这是你想要的吗?
  • retrieveParamCacheMap() 设置为私有,对它的任何访问都会经过这个加载方法。但是,你的观点很清楚。为了更安全,可能需要进一步分开。
  • 只有当第二个(以及任何后续)线程执行此代码,而第一个线程正在处理它并持有锁时,您的解决方案才有效。但是一旦它完成并释放锁,下一个线程将在tryLock() 上成功,因此开始创建新地图。

标签: java multithreading concurrency locking


【解决方案1】:

我更喜欢使用读锁和写锁的更具弹性的方法。比如:

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

这种形式的主要好处是读取访问不会阻止其他读取访问,但在重建期间一切都会干净地停止 - 甚至是其他重建。

【讨论】:

    【解决方案2】:

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

    如果您的数据很简单,请使用 ConcurrentHashMap 或类似方法来保存您的数据。然后不管在线程中读写。

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • 这是我的出发点。就我而言,双重检查锁定的问题是我有时还需要重新加载缓存。这意味着空值检查仅适用于应用程序加载,但在重新加载时无效,例如,来自外部来源的数据库更改。
      • 双重检查锁定是无效的初始化模式。
      • @xTrollxDudex,DCL 从 Java 5 开始就可以正常工作,但在这种情况下似乎无济于事。
      • @Zaan,你应该在描述中指出这一点,因为至少对我来说并不明显
      • @AngryJuice 哦,拜托。在 Java 5.0 发布后,甚至 JCIP 也不赞成使用 DCL
      猜你喜欢
      • 2011-07-18
      • 2022-06-23
      • 1970-01-01
      • 2012-04-27
      • 2017-02-15
      • 2010-09-05
      • 2022-08-22
      • 1970-01-01
      相关资源
      最近更新 更多