Как реализовать потоковую безопасную ленивую инициализацию HashMap при получении значения в Java? - PullRequest
3 голосов
/ 29 февраля 2020

Я хочу реализовать утилиту, получающую объект Enum по его строковому значению. Вот моя реализация.

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. Но почему он наберет null после вызова get(symbol) из innerMap?

Пожалуйста, помогите, это сводит меня с ума весь день!

Ответы [ 4 ]

2 голосов
/ 29 февраля 2020

Проблема связана со строкой

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

С JavaDo c из LinkedList :

Обратите внимание, что эта реализация не синхронизирована. Если несколько потоков одновременно получают доступ к связанному списку, и хотя бы один из потоков структурно изменяет список, он должен быть синхронизирован извне. (Структурная модификация - это любая операция, которая добавляет или удаляет один или несколько элементов; простая установка значения элемента не является структурной модификацией.) Обычно это выполняется путем синхронизации с некоторым объектом, который естественным образом инкапсулирует список. Если такого объекта не существует , список должен быть "обернут", используя метод Collections.synchronizedList. Это лучше всего делать во время создания, чтобы предотвратить случайный несинхронизированный доступ к списку: List list = Collections.synchronizedList(new LinkedList(...));

Поскольку LinkedList не является потокобезопасным, и во время операции add может произойти непредвиденное поведение. Это приводит к тому, что размер resultList меньше, чем число потоков, и, следовательно, ожидаемое число меньше, чем количество результатов.
Чтобы получить правильный результат, добавьте Collections.synchronizedList, как предложено.

Хотя ваша реализация хорошо, я предлагаю вам следовать ответу Мэтта Тиммерманса для более простого и надежного решения.

2 голосов
/ 29 февраля 2020

stringEnumMap должно быть ConcurrentHashMap<String, Map<String,Enum>> и использовать computeIfAbsent для ленивой инициализации.

0 голосов
/ 29 февраля 2020

ConcurrentMap interface

Как уже отмечалось, при манипулировании Map между потоками необходимо учитывать параллелизм.

Вы можете обрабатывать параллельный доступ самостоятельно. Но в этом нет необходимости. Java поставляется с двумя реализациями Map, которые созданы для внутренней обработки параллелизма. Эти реализации реализуют интерфейс ConcurrentMap.

  • ConcurrentSkipListMap
  • ConcurrentHashMap

Первый поддерживает ключи в отсортированном порядке, реализуя интерфейс NavigableMap.

Вот таблица, которую я создал, чтобы показать характеристики всех реализаций Map в комплекте с Java 11.

Table of map implementations in Java 11, comparing their features

Вы можете найти другие сторонние реализации интерфейса ConcurrentMap.

0 голосов
/ 29 февраля 2020

попробуйте переехать if (! stringEnumMap.containsKey (enumClassName)) и return stringEnumMap.get (enumClassName) .get (symbol); в синхронизированный блок.

...