[英]How to implement thread-safe HashMap lazy initialization when getting value in Java?
我想实现一个通过字符串值获取 Enum 对象的实用程序。 这是我的实现。
字符串枚举
public interface IStringEnum {
String getValue();
}
字符串枚举实用程序
public class StringEnumUtil {
private volatile static Map<String, Map<String, Enum>> stringEnumMap = new HashMap<>();
private StringEnumUtil() {}
public static <T extends Enum<T>> Enum fromString(Class<T> enumClass, String symbol) {
final String enumClassName = enumClass.getName();
if (!stringEnumMap.containsKey(enumClassName)) {
synchronized (enumClass) {
if (!stringEnumMap.containsKey(enumClassName)) {
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
Map<String, Enum> innerMap = new HashMap<>();
EnumSet<T> set = EnumSet.allOf(enumClass);
for (Enum e: set) {
if (e instanceof IStringEnum) {
innerMap.put(((IStringEnum) e).getValue(), e);
}
}
stringEnumMap.put(enumClassName, innerMap);
}
}
}
return stringEnumMap.get(enumClassName).get(symbol);
}
}
我写了一个单元测试来测试它是否在多线程情况下工作。
StringEnumUtilTest.java
public class StringEnumUtilTest {
enum TestEnum implements IStringEnum {
ONE("one");
TestEnum(String value) {
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
private String value;
}
@Test
public void testFromStringMultiThreadShouldOk() {
final int numThread = 100;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(numThread);
List<Boolean> resultList = new LinkedList<>();
for (int i = 0; i < numThread; ++i) {
new Thread(() -> {
try {
startLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
resultList.add(StringEnumUtil.fromString(TestEnum.class, "one") != null);
doneLatch.countDown();
}).start();
}
startLatch.countDown();
try {
doneLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
assertEquals(numThread, resultList.stream().filter(item -> item.booleanValue()).count());
}
}
测试结果为:
aaa:null
java.lang.AssertionError:
Expected :100
Actual :98
它表示只有一个线程执行这行代码:
System.out.println("aaa:" + stringEnumMap.get(enumClassName));
所以初始化代码应该只由一个线程执行。
奇怪的是,执行这行代码后,某个线程的结果会为null
:
return stringEnumMap.get(enumClassName).get(symbol);
由于没有 NullPointerException, stringEnumMap.get(enumClassName)
必须返回innerMap
的引用。 但是为什么在调用innerMap
get(symbol)
后会得到null
呢?
请帮忙,它让我整天发疯!
stringEnumMap
应该是ConcurrentHashMap<String, Map<String,Enum>>
,并使用computeIfAbsent
进行延迟初始化。
问题是由于线路
List<Boolean> resultList = new LinkedList<>();
请注意,此实现不是同步的。 如果多个线程并发访问一个链表,并且至少有一个线程在结构上修改了链表,则必须进行外部同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这通常是通过同步一些自然封装列表的对象来完成的。如果不存在这样的对象,应该使用 Collections.synchronizedList 方法“包装”列表。 这最好在创建时完成,以防止对列表的意外不同步访问:
List list = Collections.synchronizedList(new LinkedList(...));
由于LinkedList
不是线程安全的,因此在add
操作期间可能会发生意外行为。 这导致resultList
大小小于线程计数,因此预期计数小于结果计数。
要获得正确的结果,请按照建议添加Collections.synchronizedList
。
尽管您的实施很好,但我建议您遵循 Matt Timmermans 的答案以获得更简单和强大的解决方案。
尝试移动
if (!stringEnumMap.containsKey(enumClassName))
和
return stringEnumMap.get(enumClassName).get(symbol);
进入同步块。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.