Параллелизм Java, увеличивающий значение - PullRequest
4 голосов
/ 15 декабря 2011

Я читал о volatile и synchronized на Java, но в замешательстве почесал голову.Я надеюсь, что кто-то может помочь мне разобраться в проблеме

private HashMap<String,int> map = new HashMap<String,int>();

В моей теме

if (map.get("value") == null)
{
map.put("value",0);
}
map.put("value",map.get("value")+1);

Моя цель состоит в том, чтобы все темы поделились этим map.Если я добавлю volatile, это, похоже, не решит проблему для меня (я вывожу и вижу, что map переопределяется каждый раз).Затем я попытался использовать ConcurrentHashMap и добавить volatile перед этим ... это тоже не сработало.Основываясь на моем прочтении о volatile, я понимаю, что он должен «заблокировать» доступ к map, когда записывается map, а затем, когда map запись записывается в блокировку, снимается.

Итак ... тогда я попытался добавить static

private static ConcurrentHashMap<String,int> map = new ConcurrentHashMap<String,int>();

, и это, кажется, работает отлично ... Но ... Я продолжаю читать, что использование static не является правильным способомиз-за чего-то насчет «раздора» (чего я не совсем понимаю)

Заранее спасибо

Ответы [ 3 ]

10 голосов
/ 15 декабря 2011

Volatile здесь не поможет.Volatile полезно для решения проблем с видимостью , но вы столкнулись с другой проблемой: атомарность .

О, и volatile не имеет абсолютно никакого отношения к замок * * +1011.Он не получит блокировку при чтении / записи, он никогда не освободит ничего.Что он делает, так это: все действия, которые выполнялись до a записи в энергозависимое поле, будут видны всем другим потокам после того, как они прочитают одно и то же энергозависимое поле.Блокировка не задействована (они похожи в том, что эффекты освобождения / получения блокировки в памяти одинаковы).

Операции get и set не являются атомарными, что означает, что могут произойти другие вещимежду двумя.

Например, один поток будет get значение, затем ДРУГОЙ поток будет get то же значение, оба будут увеличивать значение, затем первый будет set новое значение,тогда второй будет делать то же самое.Окончательный результат не соответствует ожидаемому.

Наиболее распространенным решением этой проблемы является сериализация доступа (т. Е. synchronize) к общей переменной или использование сравнить и установить (CAS) (поэтому вам не нужно выполнять синхронизацию).

1.synchronized

private final Map<String, Integer> m = new ConcurrentHashMap<String, Integer>();
synchronized incrementValue(final String valueName) {
  m.put(valueName, m.get(valueName) + 1);
}

Обратите внимание, что если вы используете это решение, то КАЖДЫЙ ДОСТУП к карте должен синхронизироваться с одним и тем же замком .

2.CAS

Многие алгоритмы CAS уже реализованы в JVM очень производительным способом (т. Е. Они используют собственный код, а JIT может использовать инструкции, специфичные для процессора, к которым вы не можете получить доступ другими способами -проверьте класс Unsafe в Sun JVM, например).

Один класс, который может быть вам полезен, это AtomicInteger.Вы можете использовать его следующим образом:

private final Map<String, AtomicInteger> m = new ConcurrentHashMap<String, AtomicInteger>();
incrementValue(final String valueName) {
  m.get(valueName).incrementAndGet();
}

То, что будет делать алгоритм CAS, примерно так:

for (;;) {
  state = object.getCurrentState();
  if (object.updateValueAndStateIfStateDidntChange(state)) {
    break;
  }
}

Предполагается, что метод updateValueAndStateIfStateDidntChange является атомарным и возвращаетистина только в том случае, если удалось обновить значение.Таким образом, если другой поток изменяет значение после того, как вы получите состояние и перед тем, как вы обновите значение, метод вернет false и цикл попытается повторить его.

Предполагая, что вы можете реализовать этот метод таким способомкоторый не будет использовать synchronized (и вы можете, используя классы в java.util.concurrent), вы избежите конфликта (что означает потоки, ожидающие получения блокировок, удерживаемых другими потоками), и вы можете увидеть общее улучшениев производительности.

Я часто использую подобные вещи в распределенной системе выполнения задач, которую я написал.Все задачи должны быть выполнены ровно один раз, и у меня есть много машин, выполняющих задачи.Все задачи указаны в одной таблице MySQL.Как это сделать?У вас должен быть столбец, цель которого - разрешить внедрение CAS.Назовите это executing.Перед запуском задачи вы должны сделать что-то вроде: получить следующую задачу, "update tasks set executing = 1 where id = :id AND executing = 0" и подсчитать количество обновленных строк .Если вы обновили 0 строк, это происходит потому, что другой поток / процесс / машина уже выполнил эту задачу (и успешно выполнил этот запрос «обновления»);в этом случае вы забудете об этом и попробуете выполнить следующее задание, поскольку знаете, что оно уже выполняется.Если вы обновили 1 строку, то это хорошо, вы можете выполнить ее.

В другом месте, где я часто использую эту идею, CAS находится в очень динамичном (с точки зрения его конфигурации) пуле ресурсов.писал (я использую его в основном для управления «соединениями», т. е. сокетами, но он достаточно универсален для хранения любого вида ресурса ).По сути, он подсчитывает, сколько ресурсов он держит.Когда вы пытаетесь получить ресурс, он читает счетчик, уменьшает его, пытается обновить его (если ничто иное не изменило счетчик между ними), и если это успешно, то вы можете просто взять ресурс из пула и одолжить его(как только счетчик достигнет 0, он не одолжит ресурс).Если я когда-нибудь опубликую этот код, я обязательно добавлю сюда ссылку.

0 голосов
/ 03 сентября 2014

Volatile гарантирует, что потоки не будут кэшировать это значение. Volatile гарантирует, что запись / чтение этого значения из памяти является атомарным, но не обеспечивает блокировку этой переменной в многоступенчатой ​​операции.

Замените Map на ConcurrentHashMap и используйте атомарную операцию CAS.

Использование: ConcurrentHashMap.putIfAbsent ()

0 голосов
/ 15 декабря 2011

Ответ, конечно, должен использовать AtomicInteger, а не int.

И если вы используете ConcurrentHashMap, вам нужно использовать его специальный потокобезопасный методputIfAbsent().

Вот выдержка из этого метода в виде javadoc:

Это эквивалентно

if (!map.containsKey(key))
    return map.put(key, value);
else
    return map.get(key);

за исключением того, что действие выполняется атомарно.

private ConcurrentMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();

AtomicInteger value = map.get("value");
if (value == null) {
    map.putIfAbsent("value", new AtomicInteger());
    value = map.get("value");
}
value.incrementAndGet();

Обратите внимание, что вам не нужно возвращать новое значение.

Если вы сделаете это изменение в своем коде, все должно работать нормально.

...