Почему HashMap не является потокобезопасным

Java

1. Цели обучения

1. Причины небезопасности потока HashMap:

причина:

  • В JDK1.7 функция HashMap#transfer() вызывается из-за многопоточного расширения HashMap. Конкретная причина: поток приостанавливается во время процесса выполнения, другие потоки завершили миграцию данных и приостанавливаются после того, как ресурсы ЦП выпущен Поток повторно выполняет предыдущую логику, и данные были изменены, что приводит к бесконечному циклу и потере данных.
  • В JDK1.8 HashMap#putVal() вызывается из-за многопоточной операции размещения в HashMap Конкретная причина: предположим, что оба потока A и B выполняют операции размещения, а индекс вставки, вычисленный хеш-функцией, является то же самое, когда поток A завершает выполнение шестой строки кода, он приостанавливается из-за исчерпания кванта времени, а поток B вставляет элемент в нижний индекс после получения кванта времени, завершая обычную вставку, а затем поток A получает квант времени, так как оценка коллизии хэшей была выполнена ранее, все оценки не будут сделаны в это время, а вставка будет выполнена напрямую, что приведет к тому, что данные, вставленные потоком B, будут перезаписаны потоком A. , поэтому поток небезопасен.

улучшать:

  • Потеря данных и бесконечный цикл были хорошо решены в JDK 1.8. Если вы прочитаете исходный код 1.8, вы обнаружите, что HashMap#transfer() не может быть найден, потому что JDK1.8 находится непосредственно в HashMap#resize() Миграция данных завершено.

2. Воплощение небезопасности потока HashMap:

  • Небезопасность потока JDK1.7 HashMap отражается в: бесконечный цикл, потеря данных
  • Небезопасность потока JDK1.8 HashMap отражается в: покрытии данных

2. Причины небезопасности потока HashMap, бесконечного цикла, потери данных и покрытия данных

1. Небезопасность потока, вызванная расширением JDK1.7.

Небезопасность потока HashMap в основном возникает в функции расширения, которая вызывает JDK1.7 HshMap#transfer():

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

Этот код является операцией расширения HashMap, перемещает нижний индекс каждого сегмента и использует метод вставки заголовка для переноса элементов в новый массив. Метод вставки заголовка изменит порядок связанного списка, что также является ключевым моментом для формирования бесконечного цикла. После понимания метода вставки головы продолжайте видеть, как вызвать бесконечный цикл и потерю данных.

2. Расширение вызывает бесконечный цикл и потерю данных

Предположим, что есть два потока A и B, одновременно расширяющие емкость следующего HashMap:

image

Результат после нормального расширения выглядит следующим образом:

image

Но когда поток A выполняет строку 11 передаточной функции выше, квант времени процессора исчерпывается, и поток A приостанавливается. То есть, как показано на следующем рисунке:

image

На данный момент в потоке A: e=3, next=7, e.next=null

image

Когда квант времени потока A исчерпан, ЦП начинает выполнять поток B и успешно завершает миграцию данных в потоке B.

image

Дело в том, что согласно модели памяти Java, после того, как поток B выполнит миграцию данных, и newTable, и таблица в основной памяти будут обновлены, то есть: 7.next=3, 3.next=null.

Затем поток A получает квант времени процессора и продолжает выполнять newTable[i] = e, а в соответствующую позицию нового массива помещает 3. После выполнения этого раунда цикла ситуация с потоком A выглядит следующим образом:

image

Затем продолжить выполнение следующего цикла.В это время e=7.При чтении e.next из основной памяти обнаруживается, что в основной памяти 7.next=3.В это время next=3, а 7 помещается в метод вставки головы в новый массив и продолжает выполнять этот раунд циклов, результаты следующие:

image

На данный момент нет проблем.

Последний раунд next=3, e=3, можно найти следующий цикл, 3.next=null, поэтому этот раунд циклов будет последним.

Затем, когда выполняется e.next=newTable[i], то есть 3.next=7, 3 и 7 соединяются друг с другом, когда выполняется newTable[i]=e, 3 повторно вставляется путем вставки заголовка В связанном списке результат выполнения показан на следующем рисунке:

image

Как упоминалось выше, e.next=null означает next=null, когда выполняется e=null, следующий цикл выполняться не будет. К моменту завершения операций раскрытия потоков A и B очевидно, что после выполнения потока A в HashMap появится кольцевая структура, а при дальнейшем использовании HashMap появится бесконечный цикл.

И из приведенного выше рисунка видно, что элемент 5 был необъяснимо потерян во время расширения, что вызвало проблему потери данных.

3. Независимо от небезопасности в JDK1.8

Потеря данных и бесконечный цикл, вызванные вышеуказанным расширением, были хорошо решены в JDK 1.8.Если вы прочитаете исходный код 1.8, вы обнаружите, что HashMap#transfer() не может быть найден, потому что JDK1.8 находится непосредственно в HashMap. Миграция данных выполняется в #resize().

зачем говоритьБудет ли JDK1.8 иметь покрытие данных?Давайте взглянем на следующий код операции put в JDK1.8:

image

Шестая строка кода предназначена для определения наличия коллизии хэшей.Предполагая, что оба потока A и B выполняют операции размещения, а нижний индекс вставки, вычисленный хэш-функцией, одинаков, когда поток A завершает выполнение шестой строки кода. , из-за времени После того, как срез исчерпан, он приостанавливается, и поток B вставляет элемент в нижний индекс после получения среза времени, завершая обычную вставку, а затем поток A получает срез времени. столкновение было выполнено ранее, все в это время больше не принимается решение, но вставка выполняется напрямую, что приводит к тому, что данные, вставленные потоком B, перезаписываются потоком A, поэтому поток небезопасен.

Вдобавок к этому в строке 38 кода указан размер ++. Мы так думаем, что это по-прежнему потоки A и B. Когда эти два потока одновременно выполняют операции ввода, при условии, что размер zise текущий HashMap равен 10, когда поток A выполняет 38-ю строку кода, он получает значение размера 10 из основной памяти и готовится выполнить операцию +1, но поскольку квант времени исчерпан, он должен отказаться Поток B радостно получает процессор или берет его из основной памяти. До значения size 10 выполняет операцию +1, завершает операцию put и записывает size=11 обратно в основную память, затем поток A снова получает процессор и продолжает выполняться (значение размера в это время все еще равно 10), когда операция размещения завершена После этого размер = 11 все еще записывается обратно в память.В это время оба потока A и B выполняют операцию размещения операции, но значение size увеличивается только на 1, поэтому говорят, что поток небезопасен из-за перезаписи данных.

3. Как заставить HashMap выполнять потокобезопасные операции в случае многопоточности?

Используйте Collections.synchronizedMap(map), чтобы обернуть его в синхронизированную карту.Принцип заключается в синхронизации всех методов HashMap.

Например: Collections.SynchronizedMap#get()

public V get(Object key) {
    synchronized (mutex) {
        return m.get(key);
    }
}

4. Резюме

1. Причины небезопасности потока HashMap:

причина:

  • В JDK1.7 функция HashMap#transfer() вызывается из-за многопоточного расширения HashMap. Конкретная причина: поток приостанавливается во время процесса выполнения, другие потоки завершили миграцию данных и приостанавливаются после того, как ресурсы ЦП выпущен Поток повторно выполняет предыдущую логику, и данные были изменены, что приводит к бесконечному циклу и потере данных.
  • В JDK1.8 HashMap#putVal() вызывается из-за многопоточной операции размещения в HashMap Конкретная причина: предположим, что оба потока A и B выполняют операции размещения, а индекс вставки, вычисленный хеш-функцией, является то же самое, когда поток A завершает выполнение шестой строки кода, он приостанавливается из-за исчерпания кванта времени, а поток B вставляет элемент в нижний индекс после получения кванта времени, завершая обычную вставку, а затем поток A получает квант времени, так как оценка коллизии хэшей была выполнена ранее, все оценки не будут сделаны в это время, а вставка будет выполнена напрямую, что приведет к тому, что данные, вставленные потоком B, будут перезаписаны потоком A. , поэтому поток небезопасен.

улучшать:

  • Потеря данных и бесконечный цикл были хорошо решены в JDK 1.8. Если вы прочитаете исходный код 1.8, вы обнаружите, что HashMap#transfer() не может быть найден, потому что JDK1.8 находится непосредственно в HashMap#resize() Миграция данных завершено.

2. Воплощение небезопасности потока HashMap:

  • Небезопасность потока JDK1.7 HashMap отражается в: бесконечный цикл, потеря данных
  • Небезопасность потока JDK1.8 HashMap отражается в: покрытии данных

5. Ссылка

blog.CSDN.net/Смотреть Fort_ocean/… классная оболочка.cai/articles/96…