Java String.intern() 使用 HashTable 而不是 ConcurrentHashMap

Tur*_*rac 1 java concurrency performance

我正在研究 String.intern() ,这种方法有性能损失。我将 String.intern() 与 ConcurrentHashMap.putIfAbsent(s,s) 与 Microbenchmark 进行了比较。使用Java1.8.0_212,Ubuntu 18.04.2 LTS

\n\n
@Param({"1", "100", "10000", "1000000"})\nprivate int size;\n\nprivate StringIntern stringIntern;\nprivate ConcurrentHashMapIntern concurrentHashMapIntern;\n\n@Setup\npublic void setup(){\n    stringIntern = new StringIntern();\n    concurrentHashMapIntern = new ConcurrentHashMapIntern();\n}\npublic static class StringIntern{\n    public String intern(String s){\n        return s.intern();\n    }\n}\npublic static class ConcurrentHashMapIntern{\n    private final Map<String, String> map;\n\n    public ConcurrentHashMapIntern(){\n        map= new ConcurrentHashMap<>();\n    }\n    public String intern(String s){\n        String existString = map.putIfAbsent(s, s);\n        return (existString == null) ? s : existString;\n    }\n}\n\n@Benchmark\npublic void intern(Blackhole blackhole){\n    for(int count =0; count<size; count ++){\n        blackhole.consume(stringIntern.intern("Example "+count));\n    }\n}\n@Benchmark\npublic void concurrentHashMapIntern(Blackhole blackhole){\n    for(int count =0; count<size; count++){\n        blackhole.consume(concurrentHashMapIntern.intern("Example " +count));\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

结果符合预期。搜索字符串时,ConcurrentHashMap 比 String.intern() 更快。

\n\n
Benchmark                             (size)  Mode  Cnt        Score        Error  Units\nMyBenchmark.concurrentHashMapIntern        1  avgt    5        0.056 \xc2\xb1      0.007  us/op\nMyBenchmark.concurrentHashMapIntern      100  avgt    5        6.094 \xc2\xb1      2.359  us/op\nMyBenchmark.concurrentHashMapIntern    10000  avgt    5      787.802 \xc2\xb1    264.179  us/op\nMyBenchmark.concurrentHashMapIntern  1000000  avgt    5   136504.010 \xc2\xb1  17872.866  us/op\nMyBenchmark.intern                         1  avgt    5        0.129 \xc2\xb1      0.007  us/op\nMyBenchmark.intern                       100  avgt    5       13.700 \xc2\xb1      2.404  us/op\nMyBenchmark.intern                     10000  avgt    5     1618.514 \xc2\xb1    460.563  us/op\nMyBenchmark.intern                   1000000  avgt    5  1027915.854 \xc2\xb1 638910.023  us/op\n
Run Code Online (Sandbox Code Playgroud)\n\n

String.intern() 比 ConcurrentHashMap 慢,因为 String.intern() 是原生 HashTable 实现。然后,阅读有关 HashTable 的javadoc,该文档说明:

\n\n
\n

如果不需要线程安全的实现,建议使用HashMap代替Hashtable。如果需要线程安全的高并发实现,那么建议使用ConcurrentHashMap代替Hashtable。

\n
\n\n

这是非常令人困惑的情况。它推荐ConcurrentHashMap,但它使用HashTable虽然性能有所损失。有谁知道为什么使用 ConcurrentHashMap 的本机 HashTable 实现实例?

\n

Ste*_*n C 6

这里发生了很多事情:

  1. 您的基准测试有非常大的误差线。重复计数可能太小。这使得结果值得怀疑

  2. 看起来您的基准测试并没有在每次运行1之后重置“interned string”缓存。因此,这意味着缓存正在增长,并且每次重复都会以不同的条件开始。这可以解释误差线......

  3. 您的ConcurrentHashMap功能在功能上不等同于String::intern. 后者使用Reference对象的本机等效项来确保可以对内部字符串进行垃圾收集。你的ConcurrentHashMap实现没有。为什么这很重要?

    • 你的ConcurrentHashMap内存泄漏严重。
    • 引用机制在 GC 时是昂贵的。(虽然可能比内存泄漏便宜2。 )

String.intern() 比 ConcurrentHashMap 慢,因为 String.intern() 是原生 HashTable 实现。

不。真正的原因是本机实现的做法不同:

  • 内部表现形式不同。本机 ( intern) 字符串池使用以本机代码实现的自定义哈希表。
  • 它必须处理影响 GC 性能的引用。
  • 还有与字符串重复数据删除和其他事情的幕后交互。

请注意,这些内容在不同的 Java 版本中差异很大。

这是非常令人困惑的情况。它推荐ConcurrentHashMap,但它使用HashTable虽然性能有所损失。

现在你正在谈论一个不同的场景,它与你正在做的事情无关。

  • 请注意,String::intern不使用HashTableor HashMap;往上看。

  • 您找到的引用是关于如何从哈希表获得良好的并发性能。您的基准是(AFAIK)单线程。对于串行用例,HashMap将提供比其他用例更好的性能。

有谁知道为什么使用本机HashTable实现实例ConcurrentHashMap

它不使用哈希表;往上看。HashTable没有或HashMap或 的原因有很多ConcurrentHashMap

  • 就是它更加注重内存的利用率。所有 Java 哈希表实现都需要大量内存,这使得它们不适合通用字符串驻留。
  • 使用类的内存和 CPU 开销Reference非常大。
  • 计算新创建的长度为 N 的字符串的哈希值的复杂度为 O(N),这在嵌入可能有数百/数千个字符长的字符串时非常重要。

最后,请小心,不要在这里关注错误的问题。如果您因为实习是应用程序的瓶颈而尝试优化实习,则另一种策略是根本不实习。在实践中,它很少节省内存(特别是与G1GC的字符串去重相比)并且很少提高字符串处理性能。


总之:

  • 您正在比较苹果和橙子。您的基于地图的实现并不等同于本机实习。
  • String::intern不仅仅(甚至主要)针对速度进行优化。
  • 通过关注速度,您会忽略内存利用率......以及内存利用率对速度的次要影响。
  • 考虑根本不实习的潜在优化。

1 - 在本机intern情况下,我认为这是不可能的。
2 - 常规堆中的 Java 内存泄漏会影响长期 GC 性能,因为保留的对象需要由 GC 重复标记和复制。也可能有副作用。