6

我在wiki 页面中阅读了有关缓存的代码示例/文档。我看到回调RemovalListener可用于拆除被驱逐的缓存对象等。我的问题是在调用提供的RemovalListener. 让我们考虑文档中的代码示例:

CacheLoader<Key, DatabaseConnection> loader = 
                                 new CacheLoader<Key, DatabaseConnection> () {
  public DatabaseConnection load(Key key) throws Exception {
    return openConnection(key);
  }
};
RemovalListener<Key, DatabaseConnection> removalListener =
                          new RemovalListener<Key, DatabaseConnection>() {
  public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
    DatabaseConnection conn = removal.getValue();
    conn.close(); // tear down properly
  }
};

return CacheBuilder.newBuilder()
  .expireAfterWrite(2, TimeUnit.MINUTES)
  .removalListener(removalListener)
  .build(loader);

这里的缓存被配置为在创建后 2 分钟驱逐元素(我知道它可能不是精确的两分钟,因为驱逐将与用户读/写调用等一起被捎带。)但无论是什么时候,图书馆都会检查那里传递给的对象是否没有活动引用RemovalListener?因为我可能有另一个线程很早就从缓存中获取了对象,但可能仍在使用它。在那种情况下,我不能close()从 RemovalListener 调用它。

的文档RemovalNotification还说:删除单个条目的通知。如果它们已经被垃圾回收,则键和/或值可能为空。 所以根据它conn可能是null在上面的例子中。在这种情况下,我们如何正确地拆除 conn 对象?在这种情况下,上面的代码示例也会抛出NullPointerException.

我试图解决的用例是:

  1. 缓存元素需要在创建两分钟后过期。
  2. 被驱逐的对象需要是closed,但只有在确保没有人使用它们之后。
4

1 回答 1

9

番石榴贡献者在这里。

我的问题是,在调用提供的 RemovalListener 之前,库是否确保该对象没有被任何其他线程使用。

不,这对番石榴来说是不可能的——而且无论如何都是个坏主意!如果缓存值为Integers,那么因为Integer.valueOf重用Integer了小于 128 的整数的对象,所以您永远不会使值低于 128 的条目过期。那将是bad

RemovalNotification 的文档还说:删除单个条目的通知。如果它们已经被垃圾回收,则键和/或值可能为空。所以根据它 conn 在上面的例子中可能是空的。

需要明确的是,这只有在您使用weakKeysweakValues或时才有可能softValues。(而且,正如您正确推断的那样,如果您需要对值进行一些拆解,则不能真正使用其中任何一个。)如果您只使用其他形式的到期,您将永远不会得到 null键或值。

一般来说,我认为基于 GC 的解决方案不会在这里工作。您必须对连接有一个强引用才能正确关闭它。(覆盖finalize()可能在这里起作用,但这通常是一件坏事。)

相反,我的方法是缓存对某种包装器的引用。就像是

 class ConnectionWrapper {
   private Connection connection;
   private int users = 0;
   private boolean expiredFromCache = false;
   public Connection acquire() { users++; return connection; }
   public void release() {
     users--;
     if (users == 0 && expiredFromCache) {
       // The cache expired this connection.
       // We're the only ones still holding on to it.
     }
   }
   synchronized void tearDown() {
     connection.tearDown();
     connection = null; // disable myself
   }

 }

然后使用 aCache<Key, ConnectionWrapper>和 aRemovalListener看起来像...

 new RemovalListener<Key, ConnectionWrapper>() {
   public void onRemoval(RemovalNotification<Key, ConnectionWrapper> notification) {
     ConnectionWrapper wrapper = notification.getValue();
     if (wrapper.users == 0) {
       // do the teardown ourselves; nobody's using it
       wrapper.tearDown();
     } else {
       // it's still in use; mark it as expired from the cache
       wrapper.expiredFromCache = true;
     }
  }
}

acquire()...然后强制用户正确使用release()

我认为真的没有比这种方法更好的方法了。检测到没有其他对连接的引用的唯一方法是使用 GC 和弱引用,但是如果没有对它的强引用,就不能拆除连接——这会破坏整个观点。您无法保证RemovalListener需要断开连接的是连接用户还是连接用户,因为如果用户花费超过两分钟的时间来完成它的操作怎么办?我认为这可能是唯一可行的方法了。

(警告:上面的代码假设一次只有一个线程在做事;它根本不同步,但希望如果你需要它,那么这足以让你了解它应该如何工作。)

于 2012-07-19T15:24:56.983 回答