ConcurrentMap 按需加载 java

ConcurrentMap on demand loading java

我正在处理需要线程安全的按需缓存。我有大约 30K + 项目的数据(在一个文件中),我只想在我的多线程游戏需要时获取这些数据。但是我不确定我的方法是否应该如何使用 ConcurrentMap 的 computeIfAbsent,如果不是,我有什么选择可以从单个文件中懒惰地加载内容而不用担心线程问题?如果对象存在于我的地图中,我想避免锁定,我已经使用 CHM 读取了它。

我已经预先缓存了要加载的文件名(即 ID),以确保它们存在以避免通过 headers 哈希映射进行持续检查。 headers 地图是只读的,只会在我的程序启动时加载一次。

这是我所做的:

private static final ConcurrentMap<Integer, ItemData> items = new ConcurentHashMap<>();
private static final HashMap<Integer, Byte> headers = new HashMap<>(); // pre loaded file names to avoid checking if file exists

public static ItemData getItem(int itemID) {
   var item = items.get(itemID);
   if (item != null) {
      return item;
   }
   // if item doesn't exist in map, check if it exists in file on disk
   if (!headers.containsKey(itemID)) {
      return null;
   }
   // if item exists in file add it to cache
   return items.computeIfAbsent(itemID, k -> {
            try (var dis = new DataInputStream(new FileInputStream("item.bin"))) {
                var data = new ItemData(itemID);
                data.load(dis); // obtains only data for one item
                return item;
            } catch (IOException e) {
               // ommited for brevity. logging goes here.
               return null;
            }
        });
}

更新: 预加载对我来说不是一个选项,我同意这样做可以解决线程问题,因为它只是只读的。但是我的游戏资产加起来总大小超过 2GB。我不想在启动期间加载所有内容,因为文件中的某些项目可能永远不会被使用。因此,我正在寻找一种仅在需要时加载它们的方法。

你写了

I want to avoid locking if the object exists in my map, which I've read using CHM does on reads.

我不知道你在哪里读到的,但这绝对是错误的。它甚至不是一个过时的声明,甚至 the very first version 指定:

Retrieval operations (including get) generally do not block…

您方法的总体结构很好。在第一次并发访问一个键的情况下,有可能多个线程通过了第一次检查,但只有一个线程会在 computeIfAbsent 中进行实际检索,并且所有线程都将使用结果。对已加载项目的后续访问可能会受益于第一次普通 get 访问。

还有待改进。

return items.computeIfAbsent(itemID, k -> {
    try (var dis = new DataInputStream(new FileInputStream("item.bin"))) {
        var data = new ItemData(k);
        data.load(dis); // obtains only data for one item
        return item;
    } catch (IOException e) {
       // may still do logging here
       throw new UncheckIOException(e);
    }
});

首先,虽然这是进行日志记录的好方法(为简洁起见省略了它),但返回 null 并强制调用代码处理 null 并不是一个好主意。您已经有了 headers.containsKey(…) 检查,告诉我们资源应该在那里,因此应用程序可能无法处理缺失的情况,所以我们讨论的是一种特殊情况。

此外,您可以使用传递给函数的 k 参数,而不是从周围范围访问 itemID。限制访问范围不仅更干净,在这种情况下,它会将 lambda 表达式变成一个非捕获表达式,这意味着它不需要每次都创建一个新对象,否则需要它来保存捕获的值.


如果你真的为所有ItemData读取同一个item.bin文件,你可以考虑使用内存映射I/O来共享数据,而不是用[=20]读取=].内存映射文件的 ByteBuffer 表示提供了几乎相同的方法来获取复合项,它甚至支持 DataInputStream 不支持的小端处理。