Hazelcast IMap 中用于计算方法的无限循环
Infinite Loop in Hazelcast IMap for compute method
我尝试使用 Set
接口作为 hazelcast IMap
实例的值,当我 运行 我的测试时,我发现测试挂在 ConcurrentMap#compute
方法中。
为什么我在这段代码中使用hazelcast
IMap
时会出现死循环:
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.IMap;
import java.io.Serializable;
import java.util.*;
public class Main {
public static void main(String[] args) {
IMap<String, HashSet<StringWrapper>> store = Hazelcast.newHazelcastInstance(
new Config().addMapConfig(new MapConfig("store"))
).getMap("store");
store.compute("user", (k, value) -> {
HashSet<StringWrapper> newValues = Objects.isNull(value) ? new HashSet<>() : new HashSet<>(value);
newValues.add(new StringWrapper("user"));
return newValues;
});
store.compute("user", (k, value) -> {
HashSet<StringWrapper> newValues = Objects.isNull(value) ? new HashSet<>() : new HashSet<>(value);
newValues.add(new StringWrapper("user"));
return newValues;
});
System.out.println(store.keySet());
}
// Data class
public static class StringWrapper implements Serializable {
String value;
public StringWrapper() {}
public StringWrapper(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
StringWrapper value = (StringWrapper) o;
return Objects.equals(this.value, value.value);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), value);
}
}
}
Hazelcast:3.9.3
Java:build 1.8.0_161-b12
操作系统:macOS High Sierra 10.13.3
@Alykoff 我根据上面的示例和 ArrayList 版本重现了这个问题,它被报告为 github 问题:https://github.com/hazelcast/hazelcast/issues/12557.
有 2 个不同的问题:
1 - 使用 HashSet 时,问题是 Java 如何反序列化 HashSet/ArrayList(集合)以及 compute
方法如何工作。在 compute
方法中(因为 Hazelcast 遵守 Java 6 并且没有 compute
方法可以重写,默认实现来自 ConcurrentMap
调用),这个块导致无限循环:
// replace
if (replace(key, oldValue, newValue)) {
// replaced as expected.
return newValue;
}
// some other value replaced old value. try again.
oldValue = get(key);
此replace
方法调用 IMap 替换方法。 IMap 检查当前值是否等于 user-supplied 值。但是由于 Java 序列化 optimization
,检查失败。请检查 HashSet.readObject
方法。您会看到,在反序列化 HashSet 时,由于元素大小已知,它会创建容量为:
的内部 HashMap
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
但是你的HashSet
,创建时没有初始容量,默认容量为16,而反序列化的初始容量为1。这改变了序列化,索引51包含当前容量&它似乎 JDK re-calculate 它在反序列化对象以最小化大小时基于大小。
请看下面的例子:
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IMap<String, Collection<String>> store = instance.getMap("store");
Collection<String> val = new HashSet<>();
val.add("a");
store.put("a", val);
Collection<String> oldVal = store.get("a");
byte[] dataOld = ((HazelcastInstanceProxy) hz).getSerializationService().toBytes(oldVal);
byte[] dataNew = ((HazelcastInstanceProxy) hz).getSerializationService().toBytes(val);
System.out.println(Arrays.equals(dataNew, dataOld));
此代码打印 false
。但是,如果您创建初始大小为 1
的 HashSet
,则两个字节数组是相等的。在你的情况下,你不会得到无限循环。
2 - 使用 ArrayList
或任何其他集合时,还有您在上面指出的另一个问题。由于 compute
方法在 ConcurrentMap
中的实现方式,当您将旧值分配给 newValue
并添加新元素时,您实际上修改了 oldValue
从而导致替换方法失败.但是当您将代码更改为 new ArrayList(value)
时,现在您正在创建一个 new ArrayList
& value
集合未被修改。如果您不想修改原始集合,最好在使用集合之前对其进行包装。如果由于我解释的第一个问题而使用大小 1
创建,则 HashSet
同样适用。
所以在你的情况下,你应该使用
Collection<String> newValues = Objects.isNull(value) ? new HashSet<>(1) : new HashSet<>(value);
或
Collection<String> newValues = Objects.isNull(value) ? new ArrayList<>() : new ArrayList<>(value);
那个 HashSet
案例似乎是一个 JDK 问题,而不是优化。我不知道这些情况中的任何一个都可以在 Hazelcast 中 solved/fixed,除非 Hazalcast 覆盖 HashXXX
集合序列化并覆盖 compute
方法。
我尝试使用 Set
接口作为 hazelcast IMap
实例的值,当我 运行 我的测试时,我发现测试挂在 ConcurrentMap#compute
方法中。
为什么我在这段代码中使用hazelcast
IMap
时会出现死循环:
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.IMap;
import java.io.Serializable;
import java.util.*;
public class Main {
public static void main(String[] args) {
IMap<String, HashSet<StringWrapper>> store = Hazelcast.newHazelcastInstance(
new Config().addMapConfig(new MapConfig("store"))
).getMap("store");
store.compute("user", (k, value) -> {
HashSet<StringWrapper> newValues = Objects.isNull(value) ? new HashSet<>() : new HashSet<>(value);
newValues.add(new StringWrapper("user"));
return newValues;
});
store.compute("user", (k, value) -> {
HashSet<StringWrapper> newValues = Objects.isNull(value) ? new HashSet<>() : new HashSet<>(value);
newValues.add(new StringWrapper("user"));
return newValues;
});
System.out.println(store.keySet());
}
// Data class
public static class StringWrapper implements Serializable {
String value;
public StringWrapper() {}
public StringWrapper(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
StringWrapper value = (StringWrapper) o;
return Objects.equals(this.value, value.value);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), value);
}
}
}
Hazelcast:3.9.3
Java:build 1.8.0_161-b12
操作系统:macOS High Sierra 10.13.3
@Alykoff 我根据上面的示例和 ArrayList 版本重现了这个问题,它被报告为 github 问题:https://github.com/hazelcast/hazelcast/issues/12557.
有 2 个不同的问题:
1 - 使用 HashSet 时,问题是 Java 如何反序列化 HashSet/ArrayList(集合)以及 compute
方法如何工作。在 compute
方法中(因为 Hazelcast 遵守 Java 6 并且没有 compute
方法可以重写,默认实现来自 ConcurrentMap
调用),这个块导致无限循环:
// replace
if (replace(key, oldValue, newValue)) {
// replaced as expected.
return newValue;
}
// some other value replaced old value. try again.
oldValue = get(key);
此replace
方法调用 IMap 替换方法。 IMap 检查当前值是否等于 user-supplied 值。但是由于 Java 序列化 optimization
,检查失败。请检查 HashSet.readObject
方法。您会看到,在反序列化 HashSet 时,由于元素大小已知,它会创建容量为:
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
但是你的HashSet
,创建时没有初始容量,默认容量为16,而反序列化的初始容量为1。这改变了序列化,索引51包含当前容量&它似乎 JDK re-calculate 它在反序列化对象以最小化大小时基于大小。
请看下面的例子:
HazelcastInstance hz = Hazelcast.newHazelcastInstance();
IMap<String, Collection<String>> store = instance.getMap("store");
Collection<String> val = new HashSet<>();
val.add("a");
store.put("a", val);
Collection<String> oldVal = store.get("a");
byte[] dataOld = ((HazelcastInstanceProxy) hz).getSerializationService().toBytes(oldVal);
byte[] dataNew = ((HazelcastInstanceProxy) hz).getSerializationService().toBytes(val);
System.out.println(Arrays.equals(dataNew, dataOld));
此代码打印 false
。但是,如果您创建初始大小为 1
的 HashSet
,则两个字节数组是相等的。在你的情况下,你不会得到无限循环。
2 - 使用 ArrayList
或任何其他集合时,还有您在上面指出的另一个问题。由于 compute
方法在 ConcurrentMap
中的实现方式,当您将旧值分配给 newValue
并添加新元素时,您实际上修改了 oldValue
从而导致替换方法失败.但是当您将代码更改为 new ArrayList(value)
时,现在您正在创建一个 new ArrayList
& value
集合未被修改。如果您不想修改原始集合,最好在使用集合之前对其进行包装。如果由于我解释的第一个问题而使用大小 1
创建,则 HashSet
同样适用。
所以在你的情况下,你应该使用
Collection<String> newValues = Objects.isNull(value) ? new HashSet<>(1) : new HashSet<>(value);
或
Collection<String> newValues = Objects.isNull(value) ? new ArrayList<>() : new ArrayList<>(value);
那个 HashSet
案例似乎是一个 JDK 问题,而不是优化。我不知道这些情况中的任何一个都可以在 Hazelcast 中 solved/fixed,除非 Hazalcast 覆盖 HashXXX
集合序列化并覆盖 compute
方法。