Каждый метод ConcurrentHashMap
является потокобезопасным. Но вызов нескольких методов ConcurrentHashMap
для одного и того же ключа приводит к условиям гонки. А ConcurrentHashMap
рекурсивный вызов одного и того же метода для разных ключей приводит к тупикам.
Давайте посмотрим на пример, чтобы понять, почему это происходит:
Вызов нескольких методов
В следующем тесте я использую два метода ConcurrentHashMap
для одного и того же ключа 1. Метод update
, строки с 3 по 10, сначала получает значение с ConcurrentHashMap
помощью метода get
. Затем обновление увеличивает значение и возвращает его, используя метод put
, строки 6 и 8:
Джава
x
1
импорт ком . вмленс . api . AllInterleavings ;
2
открытый класс TestUpdateWrong {
3
публичное обновление void ( ConcurrentHashMap < Integer , Integer > map ) {
4
Целочисленный результат = карта . получить ( 1 );
5
if ( result == null ) {
6
карта . пут ( 1 , 1 );
7
} еще {
8
карта . пут ( 1 , результат + 1 );
9
}
10
}
11
12
public void testUpdate () выбрасывает InterruptedException {
13
попробуй ( AllInterleavings allInterleavings =
14
new AllInterleavings ( "TestUpdateWrong" );) {
15
while ( allInterleavings . hasNext ()) {
16
final ConcurrentHashMap < Integer , Integer > map =
17
новый ConcurrentHashMap < Integer , Integer > ();
18
Первая тема = новая тема (() -> {
19
обновление ( карта );
20
});
21
Тема вторая = новая тема (() -> {
22
обновление ( карта );
23
});
24
первый . начало ();
25
второй . начало ();
26
первый . join ();
27
второй . join ();
28
assertEquals ( 2 , map . get ( 1 ). intValue ());
29
}
30
}
31
}
32
}
Чтобы проверить, что происходит, я использую два потока, созданные в строках 18 и 21. Я запускаю эти два потока в строках 25 и 25. И затем жду, пока оба не завершатся с помощью объединения потоков, в строках 26 и 27. После того, как оба потока остановлены Я проверяю, действительно ли значение равно двум, строка 28.
Чтобы проверить все чередования потоков, мы помещаем полный тест в цикл while, итерируя по всем чередованиям потоков, используя класс AllInterleavings
из vmlens , строка 15. Выполняя тест, я вижу следующую ошибку:
java.lang.AssertionError: expected:<2> but was:<1>
Чтобы понять, почему результат равен одному, а не двум, как ожидалось, мы можем взглянуть на отчет, сгенерированный vmlens:
Так. проблема в том, что сначала оба потока вызывают, get,
а после этого оба потока вызывают put
. Итак, оба потока видят пустое значение и обновляют значение до одного. Это приводит к результату один, а не, как ожидается, два. Хитрость в решении этого условия гонки заключается в использовании только одного метода вместо двух методов для обновления значения. Используя метод compute
, мы можем сделать это. Итак, правильная версия выглядит так:
public void update() {
map.compute(1, (key, value) -> {
if (value == null) {
return 1;
} else {
return value + 1;
}
});
}
Вызов одного и того же метода рекурсивно
Теперь давайте рассмотрим пример для ConcurrentHashMap
рекурсивного вызова того же метода :
public class TestUpdateRecursive {
private final ConcurrentHashMap<Integer, Integer> map =
new ConcurrentHashMap<Integer, Integer>();
public TestUpdateRecursive() {
map.put(1, 1);
map.put(2, 2);
}
public void update12() {
map.compute(1, (key,value) -> {
map.compute(2, ( k , v ) -> { return 2; } );
return 2;
});
}
public void update21() {
map.compute(2, (key,value) -> {
map.compute(1, ( k , v ) -> { return 2; } );
return 2;
});
}
@Test
public void testUpdate() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute( () -> { update12(); } );
executor.execute( () -> { update21(); } );
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
}
}
Здесь мы вызываем compute
метод внутри compute
метода для разных ключей. Один раз для ключа, один, затем два, и один раз для ключа, два, затем один. Если мы запустим тест, мы увидим следующий тупик:
Чтобы понять, почему возникает этот тупик, мы должны взглянуть на внутренности ConcurrentHashMap
. ConcurrentHashMap
использует массив для хранения сопоставления между ключами и значениями. Каждый раз, когда мы обновляем такое отображение, ConcurrentHashMap,
он блокирует элемент массива, в котором хранится отображение. Таким образом, в нашем тесте вызов для вычисления ключа один заблокировал элемент массива для ключа один. И затем мы пытаемся заблокировать элемент массива для ключа два. Но этот ключ уже заблокирован другим потоком, который вызвал compute для ключа два и пытается заблокировать элемент массива для ключа один. Тупик
Обратите внимание, что только обновления нуждаются в блокировке элемента массива. Методы, доступные только для чтения, как, например, get
не используют блокировки. Таким образом, нет проблем с использованием get
метода внутри compute
вызова.
Заключение
Использование ConcurrentHashMap
потокобезопасного способа легко. Выберите один метод, который наилучшим образом соответствует вашим потребностям, и используйте его ровно один раз для каждого ключа