Итерирует ли ConcurrentHashMap значение потокобезопасности?
В javadoc для ConcurrentHashMap является следующее:
Операции поиска (включая get) обычно не блокируются, поэтому могут перекрываться с операциями обновления (включая put и remove). Retrievals отражают результаты последних завершенных операций по обновлению с их началом. Для совокупных операций, таких как putAll и clear, одновременное извлечение может отражать вставку или удаление только некоторых записей. Аналогично, итераторы и перечисления возвращают элементы, отражающие состояние хеш-таблицы в какой-то момент времени или после создания итератора/перечисления. Они не выбрасывают ConcurrentModificationException. Однако итераторы предназначены для использования только по одному потоку за раз.
Что это значит? Что произойдет, если я попытаюсь выполнить итерацию карты двумя потоками одновременно? Что произойдет, если я поместил или удалю значение с карты при его повторении?
Ответы
Ответ 1
Что это значит?
Это означает, что каждый итератор, полученный из ConcurrentHashMap
, предназначен для использования одним потоком и не должен передаваться. Это включает в себя синтаксический сахар, который предоставляется для каждого цикла.
Что произойдет, если я попытаюсь выполнить итерацию карты двумя потоками одновременно?
Он будет работать как ожидалось, если каждый из потоков использует свой собственный итератор.
Что произойдет, если я поместил или удалю значение из карты во время его итерации?
Гарантируется, что вещи не сломаются, если вы это сделаете (эта часть того, что означает "параллельное" в ConcurrentHashMap
). Однако нет никакой гарантии, что один поток увидит изменения на карте, которые выполняет другой поток (без получения нового итератора с карты). Итератор гарантированно отражает состояние карты во время ее создания. Дальнейшие изменения могут быть отражены в итераторе, но они не обязательно должны быть.
В заключение, утверждение типа
for (Object o : someConcurrentHashMap.entrySet()) {
// ...
}
будет прекрасным (или, по крайней мере, безопасным) почти каждый раз, когда вы его увидите.
Ответ 2
Вы можете использовать этот класс для проверки двух потоков доступа и одного мутирования общего экземпляра ConcurrentHashMap
:
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
{
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
{
new ConcurrentMapIteration().run();
}
public ConcurrentMapIteration()
{
for (int i = 0; i < MAP_SIZE; i++)
{
map.put("key" + i, UUID.randomUUID().toString());
}
}
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
{
private final Map<String, String> map;
public Accessor(Map<String, String> map)
{
this.map = map;
}
@Override
public void run()
{
for (Map.Entry<String, String> entry : this.map.entrySet())
{
System.out.println(
Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'
);
}
}
}
private final class Mutator implements Runnable
{
private final Map<String, String> map;
private final Random random = new Random();
public Mutator(Map<String, String> map)
{
this.map = map;
}
@Override
public void run()
{
for (int i = 0; i < 100; i++)
{
this.map.remove("key" + random.nextInt(MAP_SIZE));
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
private void run()
{
Accessor a1 = new Accessor(this.map);
Accessor a2 = new Accessor(this.map);
Mutator m = new Mutator(this.map);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
}
}
Никакое исключение не будет выбрано.
Совместное использование одного и того же итератора между потоками доступа может привести к взаимоблокировке:
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
{
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final Iterator<Map.Entry<String, String>> iterator;
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
{
new ConcurrentMapIteration().run();
}
public ConcurrentMapIteration()
{
for (int i = 0; i < MAP_SIZE; i++)
{
map.put("key" + i, UUID.randomUUID().toString());
}
this.iterator = this.map.entrySet().iterator();
}
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
{
private final Iterator<Map.Entry<String, String>> iterator;
public Accessor(Iterator<Map.Entry<String, String>> iterator)
{
this.iterator = iterator;
}
@Override
public void run()
{
while(iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
try
{
String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
private final class Mutator implements Runnable
{
private final Map<String, String> map;
private final Random random = new Random();
public Mutator(Map<String, String> map)
{
this.map = map;
}
@Override
public void run()
{
for (int i = 0; i < 100; i++)
{
this.map.remove("key" + random.nextInt(MAP_SIZE));
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
}
}
}
private void run()
{
Accessor a1 = new Accessor(this.iterator);
Accessor a2 = new Accessor(this.iterator);
Mutator m = new Mutator(this.map);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
}
}
Как только вы начнете использовать один и тот же Iterator<Map.Entry<String, String>>
среди потоков accessor и mutator, начнет появляться java.lang.IllegalStateException
.
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentMapIteration
{
private final Map<String, String> map = new ConcurrentHashMap<String, String>();
private final Iterator<Map.Entry<String, String>> iterator;
private final static int MAP_SIZE = 100000;
public static void main(String[] args)
{
new ConcurrentMapIteration().run();
}
public ConcurrentMapIteration()
{
for (int i = 0; i < MAP_SIZE; i++)
{
map.put("key" + i, UUID.randomUUID().toString());
}
this.iterator = this.map.entrySet().iterator();
}
private final ExecutorService executor = Executors.newCachedThreadPool();
private final class Accessor implements Runnable
{
private final Iterator<Map.Entry<String, String>> iterator;
public Accessor(Iterator<Map.Entry<String, String>> iterator)
{
this.iterator = iterator;
}
@Override
public void run()
{
while (iterator.hasNext())
{
Map.Entry<String, String> entry = iterator.next();
try
{
String st =
Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
private final class Mutator implements Runnable
{
private final Random random = new Random();
private final Iterator<Map.Entry<String, String>> iterator;
private final Map<String, String> map;
public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> iterator)
{
this.map = map;
this.iterator = iterator;
}
@Override
public void run()
{
while (iterator.hasNext())
{
try
{
iterator.remove();
this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
} catch (Exception ex)
{
ex.printStackTrace();
}
}
}
}
private void run()
{
Accessor a1 = new Accessor(this.iterator);
Accessor a2 = new Accessor(this.iterator);
Mutator m = new Mutator(map, this.iterator);
executor.execute(a1);
executor.execute(m);
executor.execute(a2);
}
}
Ответ 3
Это означает, что вы не должны делиться объектом итератора между несколькими потоками. Создание нескольких итераторов и одновременное использование их в отдельных потоках прекрасное.
Ответ 4
Это может дать вам хорошее представление
ConcurrentHashMap достигает более высокого concurrency, слегка расслабляя promises, который он делает для вызывающих. Операция поиска вернет значение, вставленное последней выполненной операцией вставки, а также может вернуть значение, добавленное операцией вставки, которая выполняется параллельно (но ни в коем случае не вернет результат бессмысленности). Итераторы, возвращаемые ConcurrentHashMap.iterator(), будут возвращать каждый элемент один раз максимум и никогда не будут бросать ConcurrentModificationException, но могут или не могут отражать вставки или удаления, произошедшие с момента создания итератора. Для обеспечения безопасности потока при повторной сборке не требуется (или даже возможно) столовая блокировка. ConcurrentHashMap может использоваться как замена для synchronizedMap или Hashtable в любом приложении, которое не полагается на возможность блокировки всей таблицы для предотвращения обновлений.
Относительно этого:
Однако итераторы предназначены для использования только по одному потоку за раз.
Это означает, что при использовании итераторов, созданных ConcurrentHashMap в двух потоках, безопасно, это может привести к неожиданному результату в приложении.
Ответ 5
Что это значит?
Это означает, что вы не должны пытаться использовать один и тот же итератор в двух потоках. Если у вас есть два потока, которым необходимо перебирать ключи, значения или записи, тогда каждый из них должен создавать и использовать свои собственные итераторы.
Что произойдет, если я попытаюсь выполнить итерацию карты двумя потоками одновременно?
Не совсем ясно, что произойдет, если вы нарушите это правило. Вы можете просто запутать поведение, так же, как и вы, если (например) два потока пытаются читать со стандартного ввода без синхронизации. Вы также можете получить поведение, не связанное с потоком.
Но если в двух потоках используются разные итераторы, вы должны быть в порядке.
Что произойдет, если я поместил или удалю значение из карты во время его итерации?
Это отдельная проблема, но секция javadoc, которую вы цитируете, адекватно отвечает на нее. В принципе, итераторы являются потокобезопасными, но не определены, будут ли вы видеть эффекты любых одновременных вставок, обновлений или исключений, отраженных в последовательности объектов, возвращаемых итератором. На практике это, вероятно, зависит от того, где на карте происходят обновления.