哪种实现更好:基于WeakHashMap的缓存还是基于ThreadLocal的缓存?

3

我很难决定以下两个实现方案。我想为每个线程缓存javax.xml.parsers.DocumentBuilder对象。我的主要关注点是运行时性能 - 因此,我希望尽可能避免GC。内存不是问题。

我编写了两个POC实现,并乐于听取社区对每个实现的优缺点的意见。

谢谢大家的帮助。

选项#1 - WeakHashMap

import java.io.IOException;
import java.io.StringReader;
import java.util.WeakHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


public class DocumentBuilder_WeakHashMap {
    private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    private static final WeakHashMap<Thread, DocumentBuilder> CACHE = new WeakHashMap<Thread, DocumentBuilder>();

    public static Document documentFromXMLString(String xml) throws SAXException, IOException, ParserConfigurationException {
        DocumentBuilder builder = CACHE.get(Thread.currentThread());
        if(builder == null) {
            builder = factory.newDocumentBuilder();
            CACHE.put(Thread.currentThread(), builder);
        }

        return builder.parse(new InputSource(new StringReader(xml)));
    }

}

Option #2 - ThreadLocal

import java.io.IOException;
import java.io.StringReader;
import java.lang.ref.WeakReference;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


public class DocumentBuilder_ThreadLocal {
    private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    private static final ThreadLocal<WeakReference<DocumentBuilder>> CACHE = 
        new ThreadLocal<WeakReference<DocumentBuilder>>() {
            @Override 
            protected WeakReference<DocumentBuilder> initialValue() {
                try {
                    return new WeakReference<DocumentBuilder>(factory.newDocumentBuilder());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        };

    public static Document documentFromXMLString(String xml) throws ParserConfigurationException, SAXException, IOException {
        WeakReference<DocumentBuilder> builderWeakReference = CACHE.get();
        DocumentBuilder builder = builderWeakReference.get();

        if(builder == null) {
            builder = factory.newDocumentBuilder();
            CACHE.set(new WeakReference<DocumentBuilder>(builder));
        }

        return builder.parse(new InputSource(new StringReader(xml)));
    }
}

它们都可以实现相同的功能(将documentFromXMLString()暴露给外部),所以你会选择哪一个?

谢谢, Maxim。


第一种解决方案与第二种解决方案有很大的区别,因为在第一种解决方案中,在线程之前生成器无法被垃圾回收,而在第二个示例中,您只保留了一个弱引用指向生成器,因此它可以在任何其他引用已经被释放时随时被收集... - pgras
3个回答

6
ThreadLocal方案更好,只要不使用弱引用,而是直接使用ThreadLocal<DocumentBuilder>。访问ThreadLocal值更快,因为线程直接引用包含所有ThreadLocal值的数组,只需计算在该数组中的索引即可进行查找。请参阅ThreadLocal source以了解为什么索引计算很快(int index = hash & values.mask;)。

@Hardcoded: true,有趣的问题是,如果知道一个条目永远不会被多个线程访问,那么应该如何同步访问该映射? - pgras
1
强调一下,不要使用“WeakReference”。不是因为它的性能开销,而是它可能会被立即清除。一些程序,比如NetBeans,在HotSpot进行优化后,性能会急剧下降。使用“SoftReference”就可以了。 - Tom Hawtin - tackline
@pgras 地图本身必须同步,因为它可能会损坏。Collections.synchronizedMap(new WeakHashMap<Thread, DocumentBuilder>()); 可以解决这个问题。ThreadLocal 的后备数组仅在同一线程内访问,因此此处不需要同步。 - Hardcoded
@Cowan 还有同步开销,即使比synchronizedMap少。因此,最好的解决方案仍然是ThreadLocal。 - Hardcoded
@pgras:请查看@rix0rrr的答案,关于在不使用弱引用的情况下使用线程本地可能存在内存泄漏的问题。我希望能够听到您对这一点的评论。 - Maxim Veksler
显示剩余2条评论

4

注意!

ThreadLocal 会保留一个不确定的引用到 DocumentBuilder,其中包含了该线程的 DocumentBuilder 解析的最新 XML 文档的引用。

这有几个后果,可能被认为是内存泄漏:

  • 如果 JAXP 实现加载在 Web 应用程序中(比如 Xerces 或 Oracle 的 xmlparser2.jar),这个保留的对 DocumentBuilder 的引用将导致你的 Web 应用程序所有类在取消部署时泄漏,最终导致 OutOfMemoryError:PermGenSpace! (在 Google 上搜索更多关于此主题的信息)
  • 如果由 DocumentBuilder 解析的最新 XML 文档很大,它将一直占用内存,直到该线程上解析了一个新的 XML 文档。如果你在线程池中运行长时间运行的线程(如在 J2EE 容器中),这可能是一个问题,特别是如果需要解析许多大型文档。是的,最终 内存将被释放,但在发生之前,你可能会耗尽可用内存,而当仍存在对 DocumentBuilder 的引用时,GC 将无法清理 XML 文档。

请决定这是否与你相关...


3
弱引用哈希表(WeakHashMap)本身会失败,因为它不是线程安全的:
“像大多数集合类一样,此类未同步。”
JavaDoc的第3段)
由于同步需要时间且使用Collections.synchronizedMap无法很好地扩展,因此应该使用ThreadLocal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接