【问题标题】:Allow only first thread to update cache while other threads read只允许第一个线程更新缓存,而其他线程读取
【发布时间】:2025-12-08 18:55:01
【问题描述】:

我有多个线程读取内存缓存。我想要的行为是每 5 分钟刷新一次缓存。问题是我不知道如何解决是如何强制只有一个线程更新缓存,而多个线程仍然可以读取缓存。

这是我拥有的代码的粗略示例:

public synchronized boolean readCache(int id) {
  if (cache.hasExpired()) {
    updateCache();
  }
  return cache.get(id);
}

这行得通。但是,它限制了在任何实例中只有单个线程可以读取缓存的实现。有没有更好的方法来做到这一点?

【问题讨论】:

  • 尝试信号量/重入锁

标签: java multithreading


【解决方案1】:

我有多个线程读取内存缓存。我想要的行为是每 5 分钟刷新一次缓存。问题是我不知道如何解决是如何强制只有一个线程更新缓存,而多个线程仍然可以读取缓存。

有几种方法可以做到这一点。

  • 您可以使用ReentrantReadWriteLock 并让每个线程在缓存上获得一个读锁,然后在需要更新缓存时尝试获得一个写锁——只有一个线程会获得写锁。需要注意的是,在更新缓存时,没有线程可以读取缓存。这可能是也可能不是您想要的。

  • 另一种机制可能是使用原子类compareAndSet(...) 方法,这样如果多个线程确定需要更新缓存,则只有一个线程会获胜。伪代码是:

    get cache value
    see if cache needs updating
    try to update the atomic field with our thread-id (or other unique)
    if it worked then update the cache
    else some other thread is updating the cache, return stale value for now
    

就缓存是否陈旧之间的竞争条件而言,您要么需要像synchronized 锁一样重的东西,要么您将不得不在缓存需要的时候忍受一些陈旧的数据更新。这可能取决于更新缓存需要多长时间。线程要么在过期前一纳秒请求该值,要么不请求。如果值 必须 每 5 分钟更新一次,并且您不想使用 synchronized,那么您可能应该更频繁地更新缓存 - 比如每 4 分 50 秒 - 这样就有机会过时数据的数量减少了。

请务必注意,无论您如何更新缓存,缓存本身都需要同步其内存。缓存需要类似于ConcurrentHashMapvolatile 对象,以便线程可以读取更新的缓存值,其中一个可以在适当的时候更新值,并让其他线程看到更新。

【讨论】:

    【解决方案2】:

    这是一个经典的双重检查问题:

    // Note: the method isn't synchronized
    public boolean readCache(int id) {
      // First check - quick check outside the lock.
      // If the cache isn't expired, no need to acquire a lock
      if (cache.hasExpired()) {
        // This thread may have to update the cache, so acquire the lock:
        synchronized (someSharedLock) {
          // Under the lock, double-check a different thread didn't update the cache
          // while we were waiting for the lock
          if (cache.hasExpired()) {
            updateCache();
          }
        }
      }
    
      // The cache is now definitely not expired, use it
      return cache.get(id);
    }
    

    【讨论】:

    • 有没有这样的情况:调用cache#get时缓存可能已经过期。假设一个线程调用hasExpired返回false,然后调用cache#get时缓存刚好过期。
    • @zysaaa 由于缓存过期是基于时间的,因此这是可能的。这里的假设是,如果缓存过期,它不会被刷新,你只会得到过时的结果。为了防御它,您需要将过期时间设置为比实际预期低一点。例如,如果您想要不超过五分钟的值,并且更新缓存需要一分钟,请将过期时间设置为不超过 5-1=4 分钟,减去一些增量以确保,所以为了论证 3 :55 分钟。