这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战
译文
在低延迟应用程序中,通常通过重用可变对象来避免创建不必要的对象以减少内存压力,从而减少垃圾收集器的负载。这使得应用运行的更加稳定和抖动更少。但是,必须注意如何使用这些重用对象,否则可能会出现意外结果,例如以包含重复元素(如 [B,B])的 Set 的形式。
HashCode和Equals
Java 的内置 ByteBuffer 使用 32 位寻址提供对堆和本机内存的直接访问。Chronicle Bytes 是一个 64位寻址开源替代品,允许处理更大的内存段。 这两种类型都提供了hashCode()和equals()方法,这取决于对象底层内存段的字节内容。虽然这在许多情况下都很有用,但像这样的可变对象不应该在大多数 Java 的内置 Set 类型中使用,也不应该作为大多数内置 Map 类型的键。
注意:实际上,只有 31 位和 63 位可以用作有效的地址偏移量(例如,使用 int 和 long 偏移量参数)
可变键(Mutable Keys)
Below, a small code example is presented illustrating the problem with reused mutable objects. The code shows the use of Bytes but the very same problem exists for ByteBuffer.
在这之下,一个简单的代码示例展示重用可变对象的问题。这段代码使用 Bytes 但是也有相同的问题存在在 ByteBuffer中。
Set<CharSequence> set = new HashSet<>();
Bytes<?> bytes = Bytes.from("A");
set.add(bytes);
// 重用
bytes.writePosition(0);
// 这个存在在Set中的对象已经变化了。
bytes.write("B");
// 增加相同的Bytes对象但是现在是不同的hashCode()
set.add(bytes);
System.out.println(“set = “ + set);
上面的代码将首先添加一个带有“A”的对象作为内容,这意味着该集合包含[A]。 然后该现有对象的内容将被修改为“B”,这具有将集合更改为包含 [B] 的副作用,但会保持旧的哈希码值和相应的哈希桶不变(实际上变得陈旧)。 最后,修改后的对象再次添加到集合中,但现在在另一个哈希码下,导致相同对象的前一个条目将保留!
因此,这将产生以下输出,而不是预期的 [A, B]:
set = [B, B]
ByteBuffer 和 Bytes 对象作为 Maps 中的键
当使用 Java 的 ByteBuffer 对象或 Bytes 对象作为映射中的键或集合中的元素时,一种解决方案是使用 IdentityHashMap 或 Collections.newSetFromMap(new IdentityHashMap<>()) 来防止上述可变对象特性。 这使得对象的散列与实际的字节内容无关,而是使用 System.identityHashCode() 在对象的生命周期中永远不会改变。
另一种选择是使用对象的只读版本(例如,通过调用 ByteBuffer.asReadOnlyBuffer())并避免持有对原始可变对象的任何引用,这可能为修改所谓的只读对象的内容。
总结
在某些 Map 和 Set 实现中,使用可变对象是危险的,例如 Bytes 和 ByteBuffer,其中 hashCode() 取决于对象的内容。IdentityHashMap 可防止由于对象突变而损坏映射和集合,但使这些结构与实际字节内容无关。以前修改过的内存段对象的只读版本可能会提供替代解决方案。