需要全局清理的昂贵资源的线程安全缓存

时间:2015-08-21 13:28:18

标签: java multithreading caching

情况:

  • 需要一个昂贵的创建和非线程安全的外部资源的缓存
  • 资源需要明确清理
  • 无法挂钩每个线程的终止,但应用程序的终止
  • 代码也在Servlet容器中运行,因此无法直接使用导致系统类加载器(例如ThreadLocal)的强引用的缓存(请参阅下面的编辑)。

因此,要使用ThreadLocal,它只能将WeakReference保存到资源中,并且必须保留分离的强引用集合。代码很快变得非常复杂并且会产生内存泄漏(因为线程死亡后永远不会删除强引用)。

ConcurrentHashMap似乎是一个很好的诉讼,但它也会受到内存泄漏的影响。

还有其他选择吗?一个同步的WeakHashMap ??

(希望解决方案也可以使用给定的Supplier自动初始化,就像ThreadLocal.withInitial()

编辑:

只是为了证明类加载器泄漏是件事。使用以下命令创建一个最小的WAR项目:

public class Test {
    public static ThreadLocal<Test> test = ThreadLocal.withInitial(Test::new);
}

的index.jsp:

<%= Test.test.get() %>

访问该页面并关闭Tomcat,您将获得:

Aug 21, 2015 5:56:11 PM org.apache.catalina.loader.WebappClassLoaderBase checkThreadLocalMapForLeaks
SEVERE: The web application [test] created a ThreadLocal with key of type [java.lang.ThreadLocal.SuppliedThreadLocal] (value [java.lang.ThreadLocal$SuppliedThreadLocal@54e69987]) and a value of type [test.Test] (value [test.Test@2a98020a]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.

4 个答案:

答案 0 :(得分:1)

这似乎是典型的“弱键,强大的价值引用关键”问题。如果您将值设置为弱,即使密钥可以访问也可以收集它,如果您将密钥设置为强,则密钥也可以很强地访问。如果没有JVM的直接支持,这是无法解决的。

值得庆幸的是,有一个类提供了这一点(尽管文档中没有强调它):

java.lang.ClassValue

  

懒洋洋地将计算值与(可能)每种类型相关联。例如,如果动态语言需要为消息发送调用站点遇到的每个类构造一个消息调度表,它可以使用ClassValue来缓存为每个遇到的类快速执行消息发送所需的信息。

虽然本文档没有说这些值可能引用Class键,但是为类保留调度表的预期用例意味着通常使用带有反向引用的值。

让我们用一个小测试类来演示它:

public class ClassValueTest extends ClassValue<Method> {
    @Override
    protected Method computeValue(Class<?> type) {
        System.out.println("computeValue");
        return Arrays.stream(type.getDeclaredMethods())
            .filter(m->Modifier.isPublic(m.getModifiers()))
            .findFirst().orElse(null);
    }
    public static void main(String... arg) throws Throwable {
        // create a collectible class:
        MethodHandles.Lookup l=MethodHandles.lookup();
        MethodType noArg = MethodType.methodType(void.class);
        MethodHandle println = l.findVirtual(
            PrintStream.class, "println", MethodType.methodType(void.class, String.class));
        Runnable r=(Runnable)LambdaMetafactory.metafactory(l, "run",
            println.type().changeReturnType(Runnable.class), noArg, println, noArg)
           .getTarget().invokeExact(System.out, "hello world");
        r.run();
        WeakReference<Class<?>> ref=new WeakReference<>(r.getClass());
        ClassValueTest test=new ClassValueTest();
        // compute and get
        System.out.println(test.get(r.getClass()));
        // verify that the value is cached, should not compute
        System.out.println(test.get(r.getClass()));
        // allow freeing
        r=null;
        System.gc();
        if(ref.get()==null) System.out.println("collected");
        // ensure that it is not our cache instance that has been collected
        System.out.println(test.get(String.class));
    }
}

在我的机器上打印出来:

hello world
computeValue
public void ClassValueTest$$Lambda$1/789451787.run()
public void ClassValueTest$$Lambda$1/789451787.run()
collected
computeValue
public boolean java.lang.String.equals(java.lang.Object)

为了解释,这个测试创建了一个匿名类,就像lambda表达式一样,可以进行垃圾回收。然后,它使用ClassValueTest实例来缓存Method的{​​{1}}对象。由于Class个实例引用了它们的声明类,因此我们在这里指的是值的情况。

但是,在不再使用该类之后,它会被收集,这意味着已经收集了相关的值。因此,它对关键值的反向引用免疫。

使用另一个类的最后一个测试只是确保我们不是described here的急切垃圾收集的受害者,因为我们仍在使用缓存实例本身。

此类将单个值与类相关联,而不是每个线程的值,但应该可以将MethodClassValue组合以获得所需的结果。

答案 1 :(得分:0)

我不确定你所说的问题。请查看:https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem

一些问题:

  • 如何引用资源?
  • 资源的界面是什么?
  • 应该缓存哪些数据?
  • 什么是&#34;非线程安全资源&#34;
  • 检索资源的频率是多少?
  • 访问一个资源多长时间,有多少级别的并发性?
  • 一个线程是否多次使用该资源,这就是预期缓存的原因?
  • 许多线程是否使用相同的资源(实例)?
  • 可以有多个相同资源类型的实例,因为实际的实例不是线程安全的吗?
  • 你有多少资源?
  • 是同一类型或不同类型的许多资源实例吗?

也许您可以尝试从您的问题中删除单词ThreadLocal,WeakReference,ConcurrentHashMap?

一些(疯狂的)猜测:

从我可以在行之间阅读的内容来看,在我看来,它是Java缓存的直接用例。例如。您可以使用Google Guava缓存并为显式清理添加删除侦听器。

由于资源不是线程安全的,因此您需要实现锁定机制。这可以通过将锁定对象放入缓存对象来完成。

如果您需要更多的并发性,请创建更多相同类型的资源,并使用线程的散列来增加缓存键,以模块化您想要的并发级别。

答案 2 :(得分:0)

我建议完全摆脱ThreadLocalWeakReference的东西,因为正如你所说,资源没有绑定到特定的线程,它们不能同时从多个线程访问。 / p>

而是拥有全局缓存Map <Key, Collection <Resource>>。缓存仅包含目前可以免费使用的资源。

线程首先会从缓存中请求可用资源。如果存在(当然,这应该是同步的,因为缓存是全局的),从该集合的该集合中删除任意资源并将其提供给该线程。否则,将构建一个新密钥用于该密钥。

当线程使用资源完成时,它应该将其返回到缓存,即添加到映射到资源键的集合。从那里它可以被同一个线程再次使用,甚至可以被不同的线程使用。

优点:

  • 缓存是全局的,在应用程序退出时会关闭所有已分配的资源。

  • 几乎没有任何内存泄漏的可能性,代码应该非常简洁。

  • 线程可以共享资源(假设他们在不同时间需要相同的资源),可能会减少需求。

缺点:

  • 需要同步(但可能很便宜且不难编码)。

  • 也许还有其他人,具体取决于你究竟做了什么。

答案 3 :(得分:0)

在研究弱并发地图的想法时,我发现它是在Guava的Cache中实现的。

我使用当前线程作为弱键,并提供CacheLoader来自动为每个新线程创建资源。

还添加了一个删除侦听器,以便在Thread对象为GC后或在关闭期间调用invalidateAll()方法时自动清理每个线程的资源。

上面的大多数配置也可以在一个衬里(使用lambdas)中完成。