#java #thread-safety #java.util.concurrent
#java #потокобезопасность #java.util.concurrent
Вопрос:
Мне нужно реализовать глобальный объект, собирающий статистику для веб-сервера. У меня есть Statistics
синглтон, у которого есть метод addSample(long sample)
, который впоследствии вызывается updateMax
. Очевидно, что это должно быть потокобезопасно. У меня есть этот метод для обновления максимального количества всей статистики:
AtomicLong max;
private void updateMax(long sample) {
while (true) {
long curMax = max.get();
if (curMax < sample) {
boolean result = max.compareAndSet(curMax, sample);
if (result) break;
} else {
break;
}
}
}
Правильна ли эта реализация? Я использую java.util.concurrent, потому что я считаю, что это было бы быстрее, чем просто synchronized
. Есть ли какой-то другой / лучший способ реализовать это?
Комментарии:
1. Я ценю комментарий Джона Скита о том, чтобы сначала попробовать синхронизированную версию. Я не уверен, что удержание блокировки будет стоить дороже, чем многократное нажатие на то,
volatile Long
чтоAtomicLong
удерживается.2. @EdwardThomson AtomicLong.get() реализован как энергозависимое чтение, которое на большинстве аппаратных средств в основном «бесплатное», примерно такое же дорогое, как обычное чтение из энергонезависимой памяти. Энергозависимая запись / CAS — это медленная часть. В свою очередь, один энергозависимый сервер записи / CAS примерно в два раза быстрее синхронизированного блока при низкой конкуренции. Смотрите, например mailinator.blogspot.nl/2008/03 /… и evanjones.ca/lmax-disruptor.html
Ответ №1:
Начиная с Java 8, был введен LongAccumulator. Рекомендуется как
Этот класс обычно предпочтительнее AtomicLong, когда несколько потоков обновляют общее значение, которое используется для таких целей, как сбор статистики, а не для детального управления синхронизацией. При низком уровне конкуренции за обновление два класса имеют схожие характеристики. Но при высокой конкуренции ожидаемая пропускная способность этого класса значительно выше за счет более высокого потребления пространства.
Вы можете использовать ее следующим образом:
LongAccumulator maxId = new LongAccumulator(Long::max, 0); //replace 0 with desired initial value
maxId.accumulate(newValue); //from each thread
Ответ №2:
Я думаю, что это правильно, но я бы, вероятно, немного переписал это для наглядности и определенно добавил комментарии:
private void updateMax(long sample) {
while (true) {
long curMax = max.get();
if (curMax >= sample) {
// Current max is higher, so whatever other threads are
// doing, our current sample can't change max.
break;
}
// Try updating the max value, but only if it's equal to the
// one we've just seen. We don't want to overwrite a potentially
// higher value which has been set since our "get" call.
boolean setSuccessful = max.compareAndSet(curMax, sample);
if (setSuccessful) {
// We managed to update the max value; no other threads
// got in there first. We're definitely done.
break;
}
// Another thread updated the max value between our get and
// compareAndSet calls. Our sample can still be higher than the
// new value though - go round and try again.
}
}
РЕДАКТИРОВАТЬ: Обычно я бы, по крайней мере, сначала попробовал синхронизированную версию и использовал этот код без блокировки только тогда, когда обнаружил, что это вызывает проблему.
Комментарии:
1. @Jon: Всякий раз, когда я вижу что-то подобное, я всегда обеспокоен цикличностью. Я чувствую, что да, это сработает, но я чувствую, что это открывает дверь для одновременного входа множества потоков, и все они заканчиваются непрерывным циклом, потому что значение постоянно обновляется. Насколько оправдана эта озабоченность?
2. @Erick: Они не смогут непрерывно выполнять цикл. Каждая итерация завершается либо этим потоком, либо другим — потому что мы продолжаем цикл только в том случае, если значение было обновлено, что означает, что другой поток успешно обновил его.
3. @Jon Спасибо за отличный ответ, ваш код действительно понятнее, а комментарии идеальны
4. @Erick: Просто чтобы облегчить вашу боль: в этом примере ясно, что значение может только увеличиваться. Скажем, Алиса звонит
updateMax(5)
, Боб звонитupdateMax(3)
и Чарли звонитupdateMax(1000000)
. Максимальное значение будет обновляться максимум три раза — один раз за поток. Порядок имеет очень небольшое значение. Либо он будет обновлен до 3, затем до 5, затем до 1000000, либо он будет обновлен до 3, а затем до 1000000 или до 5, а затем до 1000000, либо он будет обновлен только один раз до 1000000. Помните — как только curMax >= sample, текущий поток будет завершен.5. Я только что узнал об codereview stack exchange , может быть, этот вопрос следует перенести туда?
Ответ №3:
С Java 8 вы можете воспользоваться функциональными интерфейсами и простым выражением lamda, чтобы решить эту проблему с помощью одной строки и без зацикливания:
private void updateMax(long sample) {
max.updateAndGet(curMax -> (sample > curMax) ? sample : curMax);
}
В решении используется updateAndGet(LongUnaryOperator)
метод. Текущее значение содержится в curMax
и с помощью условного оператора выполняется простой тест, заменяющий текущее максимальное значение значением выборки, если значение выборки больше текущего максимального значения.
Ответ №4:
как будто у вас не было своего выбора ответов, вот мой:
// while the update appears bigger than the atomic, try to update the atomic.
private void max(AtomicDouble atomicDouble, double update) {
double expect = atomicDouble.get();
while (update > expect) {
atomicDouble.weakCompareAndSet(expect, update);
expect = atomicDouble.get();
}
}
это более или менее совпадает с принятым ответом, но не использует break
or while(true)
, который мне лично не нравится.
РЕДАКТИРОВАТЬ: только что обнаружен DoubleAccumulator
в java 8. в документации даже говорится, что это для сводных статистических задач, подобных вашей:
DoubleAccumulator max = new DoubleAccumulator(Double::max, Double.NEGATIVE_INFINITY);
parallelStream.forEach(max::accumulate);
max.get();
Ответ №5:
Я считаю, что то, что вы сделали, правильно, но это более простая версия, которую я также считаю правильной.
private void updateMax(long sample){
//this takes care of the case where between the comparison and update steps, another thread updates the max
//For example:
//if the max value is set to a higher max value than the current value in between the comparison and update step
//sample will be the higher value from the other thread
//this means that the sample will now be higher than the current highest (as we just set it to the value passed into this function)
//on the next iteration of the while loop, we will update max to match the true max value
//we will then fail the while loop check, and be done with trying to update.
while(sample > max.get()){
sample = max.getAndSet(sample);
}
}
Комментарии:
1. Это может временно понизить максимальное значение: если sample равен 6 max.get() возвращает 5, затем кто-нибудь обновит до 7, вы установите 6 (возвращая 7). Следующий цикл while увидит 7 > 6 и установит его (как и ожидалось). Результатом является то, что max будет равен 5 7 6 7.
2. да, пожалуйста, удалите «sample =». Из java doc метода getAndSet «Атомарно устанавливает заданное значение и возвращает старое значение».
3. @eckes прав, но нет гарантии, что порядок будет таким, как он / она говорит. это может быть
6>5
,7>5
sample=7
7>7
,sample=6
6>6
sample
,,,,,,,,,,,,,, и в конце, ,,, будет 6, а не 7… почему 6 боится 7? потому что 7 8 9. не более! 6 только что съели 7!