Для справки: пример, пояснения к коду алгоритма доступны на на этой странице с сайта IBM Developer .
На странице, указанной выше, будут указаны цели каждой операции A , B , C и D и почему они должны разрешать то, что в них упоминается как конструктивное вмешательство между потоками, одновременно обновляющими хвост очереди.
Ваше изменение повреждает алгоритм.Предложение else
должно не выполняться, если C неуспешен.Вместо этого он играет роль D , когда поток перехватывает незавершенное [*] обновление хвоста из другого потока.
[*]: то есть после C , но до D .
Чтобы понять, почему и когда это не удается, рассмотрите следующий сценарий.
while (true) {
Node<E> curTail = tail.get();
Node<E> residue = curTail.next.get();
/* (i) */
if (curTail.next.compareAndSet(null, newNode)) /* C */ {
tail.compareAndSet(curTail, newNode) /* D */ ;
return true;
} else {
tail.compareAndSet(curTail, residue) /* B */;
}
}
Два потока T1 и T2 начинаются одновременно в позиции (i) .Их стек содержит те же ссылки для curTail
и residue
.residue
предполагается null
(т.е. очередь должна находиться в состоянии покоя).
T1 завершает первый CAS C успешно.Не выполняется D .
T2 Сбой CAS C , ввод else
, выполнениеCAS B успешно, поскольку ссылка tail
не изменилась.
T1 не удается выполнить CAS D поскольку ссылка, присвоенная tail
, была установлена на null
на C .Нет отступления, и метод завершается.Элемент из T2 не вставлен.
Вторая попытка для T1 , ссылка на tail
равна null
и curTail.next
бросает NullPointerException
.Структура данных повреждена.
Подводя итог, A и B работают в парах.Они существуют для того, чтобы мешающие потоки могли помочь сближению очереди с нормальным состоянием и восстановлению из очереди, оставшейся в промежуточном состоянии.Представьте себе, что поток выполняет C , но его убивают, прежде чем он сможет запустить D .Без A и B очередь была бы навсегда повреждена. A и B обеспечивает восстановление состояния и завершение незавершенной вставки.