在guava缓存api中的RemovalListener回调是否确保没有人使用该对象

时间:2012-07-19 15:12:06

标签: java caching guava

我在wiki page中阅读了有关缓存的代码示例/文档。我看到回调RemovalListener可用于拆除被驱逐的缓存对象等。我的问题是库是否确保在调用提供的RemovalListener之前该对象未被任何其他线程使用。让我们考虑一下docs中的代码示例:

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的对象是否存在活动引用?因为我可能有另一个线程长时间从缓存中获取对象但可能仍在使用它。在这种情况下,我无法从RemovalListener上调用close()

RemovalNotification的文档也说明了: 删除单个条目的通知。如果密钥和/或值已经被垃圾收集,则它们可能为空。 因此,根据它,conn在上面的示例中可能是null。在这种情况下,我们如何正确拆除conn物体?此外,上述代码示例将抛出NullPointerException

我想解决的用例是:

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

1 个答案:

答案 0 :(得分:8)

番石榴贡献者。

  

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

不,番石榴一般不可能做到这一点 - 反正一个坏主意!如果缓存值为Integer s,那么因为Integer.valueOf对128以下的整数重用Integer个对象,所以永远不会使值低于128的条目到期。这将是 bad

  

还有RemovealNotification的文档说明:删除单个条目的通知。如果密钥和/或值已经被垃圾收集,则它们可以为null。所以根据它,conn在上面的例子中可能为null。

要明确的是,只有在您使用weakKeysweakValuessoftValues时才有可能。 (而且,正如你已经正确推断的那样,如果你需要对价值进行一些拆解,你就不能真正使用其中任何一种。)如果你只使用其他形式的过期,你将永远不会得到一个空的关键或价值。

总的来说,我认为基于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
   }

 }

然后使用Cache<Key, ConnectionWrapper> RemovalListener看起来像......

 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还是连接用户需要拆除连接,因为如果用户需要两分钟以上的时间来做什么呢?我认为这可能是唯一可行的方法。

(警告:上面的代码假定一次只有一个线程会处理;它根本不同步,但希望如果你需要它,那么这足以让你知道它应该如何工作。)