#java #multithreading #concurrency
#java #многопоточность #параллелизм
Вопрос:
У меня есть класс, в котором есть start()
stop()
метод and, и я хочу сделать следующее:
1 Один из методов не должен вызываться во время выполнения другого, например, когда start()
вызывается, то stop()
не должен вызываться одновременно.
2a Вызов любого из методов, когда он уже запущен, должен быть либо пропущен немедленно,
2b, либо они должны возвращаться одновременно с возвратом начального вызова (которому принадлежит блокировка).
Я думал, что первое требование может быть выполнено путем добавления synchronized
блока. Требование 2a, вероятно, хорошо подходит для AtomicBoolean
. Однако для меня это выглядит сложнее, чем должно быть. Существуют ли другие возможности? Может быть, существует класс, который выполняет оба требования за один раз? На самом деле не знаю, как можно достичь 2b.
Когда я просто перечитал свой пример кода, я понял, что synchronized
, вероятно, это должно быть перед isStarted
проверкой. Однако тогда я не смог сразу вернуться, когда вызов уже находится внутри метода. С другой стороны, с помощью приведенного ниже кода я не могу предотвратить, чтобы один поток сравнивал и устанавливал isStarted
true
значение, а затем приостанавливался, а другой поток stop()
выполнял логику остановки start()
, хотя логика не была завершена.
Пример кода
private final AtomicBoolean isStarted = new AtomicBoolean(false);
private final Object lock = new Object();
public void start() {
if (isStarted.compareAndSet(false, true)) {
synchronized(lock) {
// start logic
}
} else {
log.warn("Ignored attempt to start()");
}
}
public void stop() {
if (isStarted.compareAndSet(true, false)) {
synchronized(lock) {
// stop logic
}
} else {
log.warn("Ignored attempt to stop()");
}
}
Комментарии:
1. Используйте a
ReentrantLock
и егоtryLock()
метод …2. Спасибо @Holger, просто вопрос: это не выполнит 2b из вопроса, верно?
3. Политика 2a может привести к 2b по чистому совпадению, но нет способа обеспечить это.
Ответ №1:
Давайте представим этот сценарий:
- Поток A вводит ‘start’.
- Поток A занят, пытаясь разобраться в логике «запуска». Это занимает некоторое время.
- Пока A все еще занят, поэтому статус вашего кода «выполняется», но еще не «Я начал», поток B также вызывает start .
Вы сказали, что вы согласны с немедленным возвратом B.
Я сомневаюсь, действительно ли вы это имеете в виду; это означало бы, что код в B вызывает ‘start’, а затем этот вызов возвращается, но объект еще не находится в состоянии ‘started’. Конечно, вы хотите, чтобы вызов start() возвращался нормально только после того, как объект фактически был переведен в состояние «running», и возвращался через исключение, если это невозможно.
Учитывая, что я правильно интерпретировал ваши потребности, забудьте логическое значение; все, что вам нужно, синхронизировано. Помните, блокировки являются частью вашего API, и у нас, как правило, нет открытых полей в java, поэтому у вас не должно быть открытых блокировок, если вы не документируете подробно (и учитывайте, как ваш код взаимодействует с блокировкой как часть общедоступного API, который вы не можете изменить, не нарушая обратную совместимость — необычно обещание, которое вы хотите раздать случайно, так как это довольно пара наручников для будущих обновлений), итак, ваша идея создать частное поле блокировки великолепна. Однако у нас уже есть частный уникальный экземпляр, который мы можем повторно использовать для блокировок:
private final AtomicBoolean isStarted = new AtomicBoolean(false);
public void start() {
synchronized (isStarted) {
if (isStarted.compareAndSet(false, true)) startLogic();
}
}
public void stop() {
synchronized (isStarted) {
if (isStarted.compareAndSet(true, false)) stopLogic();
}
}
public boolean isStarted() {
return isStarted.get();
}
private void startLogic() { ... }
private void stopLogic() { ... }
}
Этот код гарантирует, что вызов ‘start ();’ гарантирует, что код находился в состоянии ‘started’, когда он возвращается нормально, или, по крайней мере, был (единственное исключение — если какой-то другой поток ожидал его остановки и сделал это сразу после запуска потока; предположительно,как бы странно это ни было, какое бы положение дел ни вызвало запуск stop(), это должно было произойти).
Кроме того, если вы хотите, чтобы поведение блокировки было частью API этого класса, вы можете это сделать. Например, вы могли бы добавить:
/** Runs the provided runnable such that the service is running throughout.
* Will start the service if neccessary and will block attempts to stop
* the service whilst running. Restores the state afterwards.
*/
public void run(Runnable r) {
synchronized (isStarted) {
boolean targetState = isStarted.get();
start(); // this is no problem; locks are re-entrant in java.
r.run();
if (!targetState) stop();
}
}
Комментарии:
1. Спасибо за ваш ответ. W.r.t. «B возвращается немедленно»: код запускает / останавливает соединитель, поэтому достаточно, чтобы процесс был запущен / остановлен в конечном итоге. Но, конечно, вы правы в том, что он должен быть более последовательным, как показано в вашем коде.
Ответ №2:
Ваш код может реализовать своего рода двойную проверку, подобную этой:
private final Object lock = new Object();
private volatile boolean isStarted = false;
public void start() {
if (isStarted) {
return;
}
synchronized(lock) {
if (isStarted) {
return;
}
isStarted = true;
// ...do start...
}
}
public void stop() {
if (!isStarted) {
return;
}
synchronized(lock) {
if (!isStarted) {
return;
}
isStarted = false;
// ...do stop...
}
}
Это должно соответствовать всем вашим условиям 1, 2a, 2b. Если вы выставляете isStarted, вы можете изменить флаг ПОСЛЕ, а не ДО завершения действия (do start / do stop), но в этом случае 2b будет происходить немного чаще.
Но в реальной жизни я бы предпочел использовать обмен сообщениями между потоками, а не блокировку, которая может легко привести к взаимоблокировкам (например, вы уведомляете некоторых слушателей о блокировке, а слушатели используют свои собственные блокировки).
Простой BlockingQueue может быть такой очередью обмена сообщениями. Один поток использует все команды из очереди и выполняет их одну за другой (в своем методе run()) в соответствии со своим текущим состоянием, которое сейчас не является общим. Другие потоки помещают команды запуска / остановки в очередь.