0

我最近在寻找一种为常规对象实现双缓冲线程安全缓存的方法。

之所以需要,是因为我们有一些缓存的数据结构,每个请求都会被多次命中,并且需要从一个非常大的文档(1s+ 解组时间)从缓存中重新加载,我们不能让所有请求都被延迟每分钟都长。

因为我找不到一个好的线程安全实现,所以我自己写了,现在我想知道它是否正确,是否可以做得更小......这里是:

package nl.trimpe.michiel

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Abstract class implementing a double buffered cache for a single object.
 * 
 * Implementing classes can load the object to be cached by implementing the
 * {@link #retrieve()} method.
 * 
 * @param <T>
 *            The type of the object to be cached.
 */
public abstract class DoublyBufferedCache<T> {

    private static final Log log = LogFactory.getLog(DoublyBufferedCache.class);

    private Long timeToLive;

    private long lastRetrieval;

    private T cachedObject;

    private Object lock = new Object();

    private volatile Boolean isLoading = false;

    public T getCachedObject() {
        checkForReload();
        return cachedObject;
    }

    private void checkForReload() {
        if (cachedObject == null || isExpired()) {
            if (!isReloading()) {
                synchronized (lock) {
                    // Recheck expiration because another thread might have
                    // refreshed the cache before we were allowed into the
                    // synchronized block.
                    if (isExpired()) {
                        isLoading = true;
                        try {
                            cachedObject = retrieve();
                            lastRetrieval = System.currentTimeMillis();
                        } catch (Exception e) {
                            log.error("Exception occurred retrieving cached object", e);
                        } finally {
                            isLoading = false;
                        }
                    }
                }
            }
        }
    }

    protected abstract T retrieve() throws Exception;

    private boolean isExpired() {
        return (timeToLive > 0) ? ((System.currentTimeMillis() - lastRetrieval) > (timeToLive * 1000)) : true;
    }

    private boolean isReloading() {
        return cachedObject != null && isLoading;
    }

    public void setTimeToLive(Long timeToLive) {
        this.timeToLive = timeToLive;
    }

}
4

4 回答 4

3

你写的不是线程安全的。事实上,你偶然发现了一个非常有名的常见谬误。它被称为双重检查锁定问题,许多像你这样的解决方案(这个主题有几个变体)都有问题。

对此有一些潜在的解决方案,但恕我直言,最简单的方法就是使用 ScheduledThreadExecutorService 并每分钟或经常重新加载您需要的内容。当您重新加载它时,将其放入缓存结果中,并且对它的调用只返回最新版本。这是线程安全的并且易于实现。当然,它不是按需加载的,但是除了初始值之外,您在检索值时永远不会受到性能影响。我称之为过度渴望加载而不是延迟加载。

例如:

public class Cache<T> {
  private final ScheduledExecutorsService executor =
    Executors.newSingleThreadExecutorService();
  private final Callable<T> method;
  private final Runnable refresh;
  private Future<T> result;
  private final long ttl;

  public Cache(Callable<T> method, long ttl) {
    if (method == null) {
      throw new NullPointerException("method cannot be null");
    }
    if (ttl <= 0) {
      throw new IllegalArgumentException("ttl must be positive");
    }
    this.method = method;
    this.ttl = ttl;

    // initial hits may result in a delay until we've loaded
    // the result once, after which there will never be another
    // delay because we will only refresh with complete results
    result = executor.submit(method);

    // schedule the refresh process
    refresh = new Runnable() {
      public void run() {
        Future<T> future = executor.submit(method);
        future.get();
        result = future;
        executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
      }
    }
    executor.schedule(refresh, ttl, TimeUnit.MILLISECONDS);
  }

  public T getResult() {
    return result.get();
  }
}

这需要一点解释。基本上,您正在创建一个通用接口来缓存 Callable 的结果,这将是您的文档加载。提交 Callable(或 Runnable)会返回 Future。调用 Future.get() 会阻塞,直到它返回(完成)。

因此,它的作用是根据 Future 实现 get() 方法,因此初始查询不会失败(它们会阻塞)。之后,每 'ttl' 毫秒调用一次刷新方法。它将方法提交给调度程序并调用 Future.get(),它产生并等待结果完成。完成后,它将替换“结果”成员。后续 Cache.get() 调用将返回新值。

ScheduledExecutorService 上有一个 scheduleWithFixedRate() 方法,但我避免使用它,因为如果 Callable 花费的时间超过预定的延迟,您最终会同时运行多个,然后不得不担心这个或节流。在刷新结束时提交自己的过程更容易。

于 2009-09-14T13:49:21.977 回答
0

我不确定我是否了解您的需求。对于部分值,您是否需要更快地加载(和重新加载)缓存?

如果是这样,我建议将您的数据结构分解成更小的部分。只需加载您当时需要的部分。如果将大小除以 10,则将加载时间除以与 10 相关的值。

如果可能,这可能适用于您正在阅读的原始文档。否则,这将是您阅读它的方式,您会跳过大部分内容并仅加载相关部分。

我相信大多数数据都可以分解成碎片。选择更合适的,下面是例子:

  • 通过首字母:A*, B* ...
  • 将您的 id 分成两部分:第一部分是一个类别,在缓存中查找它,如果需要则加载它,然后在里面查找您的第二部分。
于 2009-09-14T13:53:11.760 回答
0

如果您需要的不是初始加载时间,而是重新加载,也许您不介意重新加载的实际时间,但希望在加载新版本时能够使用旧版本

如果这是您的需要,我建议您将缓存设置为在字段中可用的实例(而不是静态实例)。

  1. 您使用专用线程(或至少不是常规线程)每分钟触发重新加载,这样您就不会延迟常规线程。

  2. 重新加载创建一个新实例,用数据加载它(需要 1 秒),然后简单地用新实例替换旧实例。(旧的将被垃圾收集。)用另一个对象替换一个对象是原子操作

分析:在这种情况下,任何其他线程都可以访问旧缓存,直到最后一刻?
在最坏的情况下,指令在获得旧缓存实例之后,另一个线程用新实例替换旧实例。但这不会使您的代码出错,询问旧缓存实例仍然会给出一个之前正确的值,这对于我作为第一句给出的要求是可以接受的。

为了使您的代码更正确,您可以将缓存实例创建为不可变的(没有可用的设置器,无法修改内部状态)。这更清楚地表明在多线程上下文中使用它是正确的。

于 2009-09-14T14:15:48.707 回答
-1

在您的好情况下(缓存已满且有效),您似乎锁定了更多的锁,每个请求都需要一个锁。如果缓存过期,您可以只使用锁定。

如果我们正在重新加载,什么也不做。
如果我们没有重新加载,请检查是否已过期,如果未过期则继续。如果我们没有重新加载并且我们已经过期,请获取锁定并仔细检查过期,以确保我们没有成功加载自上次检查以来。

另请注意,您可能希望在后台线程中重新加载缓存,以免一个请求被等待等待缓存填充。


    private void checkForReload() {
        if (cachedObject == null || isExpired()) {
                if (!isReloading()) {

                       // Recheck expiration because another thread might have
                       // refreshed the cache before we were allowed into the
                        // synchronized block.
                        if (isExpired()) {
                                synchronized (lock) {
                                        if (isExpired()) {
                                                isLoading = true;
                                                try {
                                                        cachedObject = retrieve();
                                                        lastRetrieval = System.currentTimeMillis();
                                                } catch (Exception e) {
                                                        log.error("Exception occurred retrieving cached object", e);
                                                } finally {
                                                        isLoading = false;
                                                }
                                        }
                                }
                        }
                }
        }

于 2009-09-14T14:17:13.933 回答