С какой целью в этом коде используется цикл?
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
С какой целью в этом коде используется цикл?
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
Классы AtomicXXX
представляют собой атомарные типы данных. Это означает, что они должны возвращать согласованные результаты при одновременном доступе к двум или более потокам. compareAndSet
- это операция, которая обычно реализуется непосредственно в аппаратном обеспечении, поэтому getAndSet
реализуется в терминах compareAndSet
.
Метод работает следующим образом: во-первых, возвращается текущее значение. Теперь возможно, что другой поток одновременно изменяет значение, поэтому его нужно проверить, используя compareAndSet
, что это не так. Если другой поток изменил значение, процедура должна быть повторена, поскольку в противном случае возвращается неправильное значение. Следовательно, цикл.
Существует школа мысли, в которой говорится, что вы должны использовать блокировки так же экономно, как можете. То есть никогда не используйте блокировку, если вы можете ее избежать, и если вы ее используете, заблокируйте ее на минимальное время. Обоснование этого объясняется иногда значительными затратами на блокировку в первую очередь, а также стоимостью одного потока ожидания, в то время как другой удерживает блокировку требуемого ресурса.
Доступно для очень долгое время, инструкции cpu, называемые Compare and Set (или CAS для краткости), предназначенные для помощи в этом что по существу делают:
if (value == providedValue) {
value = newValue;
return true;
} else {
return false;
}
эти инструкции могут выполняться на уровне машинного кода и значительно быстрее создавать блокировку.
Представьте, что вы хотите добавить 1
к номеру с помощью одной из этих инструкций таким образом, чтобы систематически работала корректно при высокой параллельной нагрузке. Очевидно, вы могли бы его кодировать как:
int old = value;
if ( compareAndSet(old, old+1) ) {
// It worked!
} else {
// Some other thread incremented it before I got there.
}
Но что мы можем сделать, если CAS
не удалось? Вы догадались - повторите попытку!
boolean succeeded = false;
do {
int old = value;
if ( compareAndSet(old, old+1) ) {
// It worked!
succeeded = true;
} else {
// Some other thread incremented it before I got there. Just try again.
}
} while (!succeeded);
И там вы видите образец, который вы наблюдаете.
Используя эту и подобные идиомы, можно реализовать множество функций и даже некоторые довольно сложные структуры данных без каких-либо блокировок (обычно называемых Lock Free). Например, здесь представляет собой реализацию Lock-Free Ring Buffer.
С какой целью в этом коде используется цикл?
Легко понять, почему существует цикл for, глядя на то, что может произойти, если его там не было.
Предположим, что метод выглядел так:
int current = get();
compareAndSet(current, newValue);
return current;
Теперь, если другой поток пришел и назвал getAndSet
одновременно, он мог бы изменить значение между
int current = get();
и
compareAndSet(current, newValue);
Не удалось выполнить compareAndSet
, и этот метод не будет работать так, как предполагалось.
Говоря это, это не единственный правильный способ реализации этого метода. Я предполагаю, что он реализован так, как это происходит из-за эффективности. Вместо того, чтобы приобретать/освобождать блокировку для этой операции, она делегирует compareAndSet
, которая, вероятно, реализована с некоторой эффективной CAS-операцией.