Java取值时如何实现线程安全的HashMap惰性初始化?

How to implement thread-safe HashMap lazy initialization when getting value in Java?

我想实现一个通过字符串值获取枚举对象的实用程序。这是我的实现。

IStringEnum.java

public interface IStringEnum {
    String getValue();
}

StringEnumUtil.java

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) 必须 return innerMap 的引用。但是为什么调用innerMapget(symbol)后会得到null

请帮忙,我整天都快疯了!

stringEnumMap 应该是 ConcurrentHashMap<String, Map<String,Enum>>,并使用 computeIfAbsent 进行惰性初始化。

尝试移动
如果 (!stringEnumMap.containsKey(enumClassName))

return stringEnumMap.get(enumClassName).get(symbol);
进入同步块。

问题出在线路

List<Boolean> resultList = new LinkedList<>();

来自JavaDoc of LinkedList

Note that this implementation is not synchronized.If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.If no such object exists, the list should be "wrapped" using the Collections.synchronizedListmethod. This is best done at creation time, to prevent accidental unsynchronized access to the list:
List list = Collections.synchronizedList(new LinkedList(...));

因为 LinkedList 不是线程安全的,在 add 操作期间可能会发生意外行为。 这导致 resultList 大小小于线程计数,因此预期计数小于结果计数。
要获得正确的结果,请按照建议添加 Collections.synchronizedList

虽然您的实施很好,但我建议您遵循 Matt Timmermans 的回答以获得更简单和可靠的解决方案。

ConcurrentMap界面

正如其他人指出的那样,如果跨线程操作 Map,您必须考虑并发性。

您可以自己处理并发访问。但没有必要。 Java 带有两个 Map 的实现,它们是为内部处理并发而构建的。这些实现实现了 ConcurrentMap 接口。

  • ConcurrentSkipListMap
  • ConcurrentHashMap

第一个按排序顺序维护键,实现NavigableMap接口。

这是我编写的 table 来展示 Map 与 Java 11.

捆绑在一起的所有实现的特征

您可能会发现 ConcurrentMap 接口的其他第三方实现。