【问题标题】:Writing a highly performant Cache编写高性能缓存
【发布时间】:2011-12-22 14:24:44
【问题描述】:

我编写了一个股票市场模拟器,它使用ConcurrentHashMap 作为缓存。

缓存包含大约 75 个元素,但它们的更新和检索速度非常快(大约每秒 500 次)。

这是我所做的:

线程 1:

连接到外部系统,该系统为我提供给定股票代码的流式报价。

线程 2(回调线程):

等待外部系统将数据传递给它。一旦它得到数据,它就会解析它,创建一个不可变的 DataEntry 对象,缓存它并向 thread3 发送信号。

线程 3(消费者线程): 收到信号后,从缓存中检索 DataEntry 并使用它。 (不让线程2直接向线程3推送数据是任务的一部分)。

public final class DataEntry{

      private final String field1;
      private final String field2;
      //...
      private final String field25;

      // Corresponding setters and getters

}

public final class Cache{

        private final Map<String, DataEntry> cache;

        public Cache( ){
           this.cache = new ConcurrentHashMap<String, DataEntry> ( 65, 0.75, 32 );
        }

        // Methods to update and retrieve DataEntry from the cache.
}

通过分析器运行它后,我注意到我正在创建一个 lotDataEntry 对象。因此伊甸园很快就会被填满。

所以,我正在考虑通过以下方式调整设计:

a) 使DataEntry 类可变。

b) 使用空的DataEntry 对象预先填充缓存。

c) 当更新到达时,从地图中检索DataEntry 对象并填充字段。

这样,DataEntry 对象的数量将是常数并且等于元素的数量。

我的问题是:

a) 此设计是否存在我通过使DataEntry 可变而引入的任何并发问题。

b)我还能做些什么来优化缓存吗?

谢谢。

【问题讨论】:

  • 您可以每秒访问超过 100 万次 ConcurrentHasMap,如果您仅每秒访问 500 次,它不会产生太大影响。
  • 如果您希望有接近 16 个或更多内核一次访问 Map,我只会增加分区大小。如果您使用少于 4 个内核一次访问地图(并且不执行任何其他操作),则不太可能产生太大影响。
  • 为什么快速填满伊甸园会成为问题?您是否真的因为 Eden GC 遇到问题?
  • 在 Java 中分配短期对象非常便宜,因此我不会担心。您可以重复使用数据条目,但仍然必须重新插入它们,所以我怀疑这会产生很大的不同。

标签: java caching optimization memory concurrenthashmap


【解决方案1】:

我不会担心 ConcurrentHashMap 的速度

Map<Integer, Integer> map = new ConcurrentHashMap<>();
long start = System.nanoTime();
int runs = 200*1000*1000;
for (int r = 0; r < runs; r++) {
    map.put(r & 127, r & 127);
    map.get((~r) & 127);
}
long time = System.nanoTime() - start;
System.out.printf("Throughput of %.1f million accesses per second%n",
        2 * runs / 1e6 / (time / 1e9));

打印

Throughput of 72.6 million accesses per second

这远远超出了您使用的访问速率。

如果你想减少垃圾,你可以使用可变对象和原语。出于这个原因,我会避免使用字符串(因为您的字符串似乎比数据条目多得多)

【讨论】:

  • 谢谢彼得。如果可以的话,还有一个问题,如果我使 DataEntry 可变,我必须在将地图添加到地图时锁定地图,不是吗?或者我可以用 AtomicReference 包装可变 DataEntry 吗?干杯(顺便说一句,喜欢你的博客)。
  • 对于高度并发的系统,默认的并发 hashmap 不是一个好主意,但我同意从描述中这似乎也不太可能(我会考虑超过几个十几个线程在这里高度并发YMMV)。无论如何,从描述来看,hashmap 似乎是一个奇怪的选择。
  • 如果您使用的是可变 DataEntry,我只会添加一次(最好在启动时)您是否需要每个仪器不止一个?为博客欢呼。 ;)
【解决方案2】:

听起来您正在使用ConcurrentHashMap,而您真正需要的是类似并发队列的东西——比如LinkedBlockingQueue

【讨论】:

  • 您好,实际上我确实需要一张地图,因为我希望相关方轮询新数据,而不是将其推送给他们。
  • 嗯...我不确定地图是否仍然是最好的数据结构。您是否考虑过为每个“相关方”简单地使用单独的队列?
【解决方案3】:
  • 一个。是的,它确实。可变的DataEntry 对象可以在没有读者注意的情况下更新,这将导致不一致的状态。
  • 乙。是的,您可以:创建一个可变的DataEntryCache,它根据请求返回不可变的DataEntry。这样,您将在读取时创建新的 DataEntry 对象,而不是在写入时。 DataEntryCache 可以在内部缓存它构造和返回的不可变 DataEntry,并在变异调用时使“缓存”无效。

编辑:我假设您缓存的原因(而不是在线程 2 和 3 之间创建队列)是除了线程 2 发送通知的条目之外,消费者线程可能会读取其他条目。如果这个假设不正确,您可能根本不需要缓存。

【讨论】:

  • +1,尽管您对 b 的回答对我来说似乎是一种竞争条件
  • 您好,我需要一张地图,以便让感兴趣的各方轮询新数据,而不是推送给他们。
  • @kdgregory 这个操作需要在DataEntryCache内部同步。
【解决方案4】:

a) 在我的代码中,对象创建经常表现为瓶颈,所以我认为您自己的重用DataEntry 对象的想法也值得实现。但是,正如 kdgregory 评论的那样,简单地覆盖当前元素将导致读取不一致的条目。因此,当更新一个条目时,改为写入一个新的或如果可用的话,一个重复使用的空闲(比如空闲几分钟)条目并将其放入映射中。将新条目放入地图后,将旧条目放在某种空闲列表中。为了完全安全,不允许读取线程访问缓存传递的 DataEntry,例如1分钟。如果线程可能阻塞,它们应该复制 DataEntry 对象,也许为此重用自己的对象。

b) 您当前的设计是模块化的,但涉及许多上下文切换,因为线程反映了模块。我会尝试一种设计,其中单个请求从开始到完成由单个线程提供服务。请求可能是对新 DataEntry 对象的完整处理。实现这一点的并发设计模式是 Leader/FollowerHalf-Sync/Half-Asynch

【讨论】:

  • “我没有看到明显的并发问题” - 如果一个线程正在从对象读取值而另一个线程正在写入值会发生什么?
  • @kdgegory,谢谢。这显而易见的;)。我更新了我的答案。
猜你喜欢
  • 2010-10-20
  • 1970-01-01
  • 1970-01-01
  • 2010-12-27
  • 2012-08-13
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
相关资源
最近更新 更多