【问题标题】:How to implement thread-safe HashMap lazy initialization when getting value in Java?在Java中获取值时如何实现线程安全的HashMap延迟初始化?
【发布时间】:2020-02-29 13:52:33
【问题描述】:

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

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

请帮忙,这让我整天发疯!

【问题讨论】:

    标签: java multithreading java.util.concurrent concurrenthashmap


    【解决方案1】:

    问题出在线路上

    List&lt;Boolean&gt; resultList = new LinkedList&lt;&gt;();

    来自JavaDoc of LinkedList

    注意这个实现是不同步的。如果多个线程同时访问一个链表,并且至少有一个线程在结构上修改了链表,它必须在外部同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这通常通过在自然封装列表的某个对象上同步来完成。如果不存在这样的对象,列表应使用 Collections.synchronizedList 方法“包装”。这最好在创建时完成,以防止意外不同步访问列表:
    List list = Collections.synchronizedList(new LinkedList(...));

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

    虽然您的实现很好,但我建议您按照 Matt Timmermans 的回答来获得更简单和强大的解决方案。

    【讨论】:

      【解决方案2】:

      stringEnumMap 应该是ConcurrentHashMap&lt;String, Map&lt;String,Enum&gt;&gt;,并使用computeIfAbsent 进行延迟初始化。

      【讨论】:

        【解决方案3】:

        ConcurrentMap接口

        正如其他人所说,如果跨线程操作Map,则必须考虑并发性。

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

        • ConcurrentSkipListMap
        • ConcurrentHashMap

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

        这是我编写的表格,用于显示与 Java 11 捆绑的 Map 的所有实现的特征。

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

        【讨论】:

          【解决方案4】:

          尝试移动
          if (!stringEnumMap.containsKey(enumClassName))

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-07-11
            • 2011-11-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-07-27
            • 1970-01-01
            相关资源
            最近更新 更多