Гарантии java.util.concurrent.locks.ReentrantLock
В контексте JDK 8-17.
Допустим, имеется ситуация, когда экземпляр ReentrantLock больше недоступен, но при этом он захвачен и имеет очередь потоков, ожидающих захвата. То есть имеется гарантия, что для этого экземпляра лока больше не будет новых вызовов lock* методов. Гарантируется также, что владелец лока или ждущие потоки также не воспользуются повторной блокировкой.
Также есть поток, который мониторит состояние этого лока.
Вопрос в следующем: есть ли гарантия, что если поток-монитор увидит false после вызова isLocked() на вышеозначенном экземпляре ReentrantLock, то это также будет означать, что очередь потоков, ожидающих захвата лока тоже пуста? То есть, !isLocked() && !hasQueuedThreads() всегда будет истинно.
Ответы (3 шт):
Пример ниже создаёт десять потоков, которые захватывают общую блокировку, ждут одну секунду отпускают блокировку и печатают done.
После задержки в одну десятую секунды запускается нить-монитор, которая ждёт нарушения условия: lock.isLocked() || !lock.hasQueuedThreads().
Если это условие нарушилось, то
- блокировка не захвачена
- в очереди к блокировке есть ждущие потоки
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static ReentrantLock lock = new ReentrantLock();
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
}
}
public static void main(String... args) {
for (int i = 0; i < 10; ++i) {
new Thread(() -> {
lock.lock();
sleep(1000);
lock.unlock();
System.out.println("done");
}).start();
}
sleep(100);
new Thread(() -> {
while (lock.isLocked() || !lock.hasQueuedThreads()) {
}
System.out.println("???????????????????????????");
}).start();
}
}
Тест стабильно показывает, что условие нарушается:
$ javac ReentrantLockExample.java $ java ReentrantLockExample done ??????????????????????????? done done done done done done done done done
Точную причину я назвать не могу. Есть какие-то догадки, но я не буду их озвучивать, так как не уверен в их правильности. Важен факт: даже если много потоков ждут блокировку, она не обязана быть всё время захвачена.
Во-первых ваше условие будет истинным в том случае, если нет блокировок и нет очереди потоков, ожидающих блокировку, т.е. не "всегда", а когда блокировки сняты и очереди на блокировку нет.
Во-вторых проблема ещё в том, что сама проверка !isLocked() && !hasQueuedThreads() без использования примитивов синхронизации вокруг неё, не гарантирует, то ваш объект не успеет сменить состояние между этими двумя проверками. Может сложиться ситуация, когда на первой проверке !isLocked() блокировка из одного потока была уже отпущена, а блокировка из другого потока ещё не началась. А в момент второй проверки !hasQueuedThreads() наоборот - последний поток уже ушёл из очереди и им была взята блокировка, но результат вашей проверки будет !false && !false -> true && true -> true, при том, что блокировка то сейчас есть в том последнем потоке ушедшем из очереди, но вы об этом не смогли узнать.
Если есть гарантия, что компилятор и среда исполнения не оптимизирует код проверки и выполняет строго в том порядке, как она написана, то обратный порядок проверки !hasQueuedThreads() && !isLocked() возможно лучше будет гарантировать результат в описанном вами кейсе. Но только возможно. Полагаться на это нельзя.
В общем случае, даже безотносительно вашего кейса и языка Java, гарантией правильного выполнения проверки x.a && x.b может быть только блокировка объекта x на время этой проверки. Без блокировки объекта x части этой проверки могут выполняться на разных состояниях объекта x, так что состояния x.a и x.b могут быть не согласованы.
P.S. Если же вы и правда хотите гарантии немного другого условия - что isLocked() || !hasQueuedThreads() всегда выполняется, как предполагают другие уважаемые комментаторы, то такой гарантии тоже нет. Как показывают опыты с кодом, при наличии очереди потоков на блокировку, между снятием блокировки одним потоком и поднятием блокировки другим потоком имеются паузы. Т.е. false || !true -> false || false -> false в такие моменты между двумя отдельными блокировками.
P.P.S.
Вопрос в следующем: есть ли гарантия, что если поток-монитор увидит
falseпосле вызоваisLocked()на вышеозначенном экземпляреReentrantLock, то это также будет означать, что очередь потоков, ожидающих захвата лока тоже пуста?
А это - вообще уже третий отдельный вопрос )) На который опять же ответ нет - потому что между отпусканием лока одним потоком и захватом его другим потоком есть пауза, так что отсутствие лока в какой-то момент времени вовсе не означает, что в этот момент нет и очереди на захват лока.
Если я правильно понял ваше подробное описание, грубо говоря, Вы желаете дождаться момента, когда вся очередь ожидающих иссякнет и последний владелец освободит блокировку.
Во-первых, в вашем описании есть "дырка". В общем случае, могут существовать потоки, которые ожидают условия, связанного с данной блокировкой. Эти потоки, на текущий момент ожидания условия, и не владеют блокировкой, и не ожидают её, но им гарантируется восстановление владения при наступлении условия:
final Lock lock = new ReentrantLock();
final Condition cond = lock.newCondition();
...
lock.lock();
try {
...
cond.await();
...
} finally {
lock.unlock();
}
Вероятно, Вы просто забыли описать, что у Вас такие потоки отсутствуют. Предположим, что это так.
Во-вторых, в спецификациях JDK для методов isLocked() и hasQueuedThreads() указано, что они не предназначены для синхронизации, но только для мониторинга. Т.е. они не имеют отношений "выполняется прежде" между собой и с другими методами, а детали реализации остаются свободными для обеспечения максимальной эффективности на разных платформах. (Пример "неработоспособности" есть в ответе @StanislavVolodarskiy)
Однако, в частном случае, если используются справедливые блокировки ReentrantLock(true), ваша задача имеет решение средствами самой блокировки:
assert lock.isFair();
lock.lock();
// Формальный момент времени, к которому должны
// выполняться все вышеперечисленные предусловия.
lock.unlock();
lock.lock();
// Здесь мы последние и единственные владельцы,
Но для обычных (несправедливых) блокировок придётся использовать дополнительные внешние средства. К примеру, поддерживать атомарные счётчики использования, которые увеличиваются до попытки захвата блокировки и уменьшаются после освобождения (это только для примера, скорее всего атомарные счётчики будут сильно тормозить, даже по сравнению со справедливыми блокировками, но могут быть иные варианты).