Java 编写高性能缓存

Java 编写高性能缓存,java,caching,optimization,memory,concurrenthashmap,Java,Caching,Optimization,Memory,Concurrenthashmap,我编写了一个股市模拟器,它使用ConcurrentHashMap作为缓存 缓存包含大约75个元素,但它们的更新和检索速度非常快(每秒约500次) 以下是我所做的: 线程1: 连接到外部系统,该系统为我提供给定股票符号的流式报价 线程2(回调线程): 等待外部系统将数据传递给它。一旦获得数据,它就会对其进行解析,创建一个不可变的DataEntry对象,缓存它并向thread3发送信号 public final class DataEntry{ private final String

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

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

以下是我所做的:

线程1:

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

线程2(回调线程):

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

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.
}
线程3(消费者线程): 收到信号后,从缓存中检索数据项并使用它。(不让thread2将数据直接推送到thread3是任务的一部分)

公共最终类数据条目{
私有最终字符串字段1;
私有最终字符串字段2;
//...
私人最终字符串字段25;
//相应的setter和getter
}
公共最终类缓存{
私有最终地图缓存;
公共缓存(){
this.cache=新的ConcurrentHashMap(65,0.75,32);
}
//从缓存中更新和检索数据项的方法。
}
在通过探查器运行它之后,我注意到我正在创建许多
DataEntry
对象。因此伊甸园很快就填满了

因此,我想通过以下方式稍微调整一下设计:

a)使
数据条目
类可变

b)用空的
数据项
对象预填充缓存

c)当更新到达时,从映射中检索
数据条目
对象并填充字段

这样,
DataEntry
对象的数量将保持不变,并等于元素的数量

我的问题是:

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

b)我还可以做些什么来优化缓存


谢谢。

听起来你在使用一个
ConcurrentHashMap
而你真正需要的是一个类似于并发队列的东西,比如说,一个?

  • a。是的。可变
    数据条目
    对象可以在读者不注意的情况下更新,这将导致不一致的状态
  • b。是的,您可以:创建一个可变的
    DataEntryCache
    ,根据请求返回不可变的
    DataEntry
    。这样,您将在读取而不是写入时创建新的
    DataEntry
    对象
    DataEntryCache
    可以在内部缓存它构造和返回的不可变的
    DataEntry
    ,并在变异调用时使该“缓存”无效
编辑:我假设缓存的原因(与在线程2和线程3之间创建队列相反)是使用者线程可以读取除线程2发送通知的条目之外的其他条目。如果这个假设不正确,您可能根本不需要缓存。

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


b)您当前的设计是模块化的,但涉及许多上下文切换,因为线程反映了模块。我会尝试一种设计,其中一个请求从开始到完成都由一个线程提供。请求可以是对新的
数据条目
对象的完整处理。实现这一点的并发设计模式是和。

我不担心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));
这远远超出了您使用的访问速率


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

您可以每秒访问ConcurrentHasMap超过一百万次,如果您每秒仅访问500次,则不会产生太大影响。如果您希望有近16个或更多的内核同时访问该映射,我只会增加分区大小。如果你一次使用少于4个核心访问地图(并且不做任何其他事情),那么这不太可能产生多大的影响。为什么让伊甸园快速填满是一个问题?由于Eden GC,您是否真的遇到了问题?在Java中,分配短期对象非常便宜,因此我不会担心单独使用。您可以重用数据条目,但仍然必须重新插入它们,所以我怀疑这会有多大区别。“我没有看到明显的并发问题”-如果一个线程正在从对象读取值,而另一个线程正在写入值,会发生什么情况?@kdgegory,谢谢。这是显而易见的。我更新了我的答案。+1,虽然你对b的答案看起来像是meHi的竞争条件,但我需要一张地图,这样我就可以让感兴趣的各方轮询新数据,而不是将其推送到他们那里。@kdgregory此操作需要在
DataEntryCache
中同步。谢谢Peter。如果可以的话,还有一个问题,如果我使数据条目可变,我必须在将地图添加到地图时锁定地图,不是吗?或者我可以用AtomicReference包装可变数据条目吗?干杯(顺便说一句,爱你的博客
Throughput of 72.6 million accesses per second