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, он не одолжит ресурс).Если я когда-нибудь опубликую этот код, я обязательно добавлю сюда ссылку.