Как как можно скорее получить блокировку среди списка блокировок?

#java #concurrency

Вопрос:

У меня есть этот чрезвычайно упрощенный класс, выполняющий задачу под блокировкой. Блокировка предназначена для предотвращения одновременного выполнения одной и той же задачи дважды. Саму задачу можно запускать столько раз, сколько потребуется.

 class Job {
  private Lock lock;
  
  // Constructor, getters, setters, etc. removed to stay short
  
  public boolean execute (int maxWaitTime) {
    if (lock.tryLock(maxWaitTime, TimeUnit.MILISECONDS)) {
      try {
        // do some lengthy job under the lock
      } finally {
        lock.unlock();
      }
    return true;
    }
  else return false;
  }
}
 

Теперь предположим, что у меня есть список этих заданий:

 List<Job> jobs = ... ;
 

Все задания в списке имеют одинаковую природу. Я хотел бы выполнить только один из них из списка, на данный момент игнорируя все остальные.
Я хочу выполнить только первую доступную задачу, ожидая как можно меньше после текущих блокировок (соответствующих текущим задачам).

Через некоторое время, скажем, через 10 секунд, если все задания все еще заняты, я сдаюсь и не выполняю ни одного (на самом деле, конечно, в этом случае происходит что-то другое).

Во-первых, я придумал это:

 for (Job job: jobs) {
  if (job.execute(10000)) return; // one job has been executed
}
 

А потом понял, что если бы у меня было N рабочих мест, то я мог бы в конечном итоге подождать в общей сложности 10*N несколько секунд вместо всего 10, прежде чем сдаться, на случай, если свободной работы не будет и ни одна не станет доступной в то же время.

Затем я подумал об этом:

 long start = System.currentTimeMillis();
while (true) {
  for (Job job: jobs) {
    if (System.currentTimeMillis() -start > 10000) return; // give up after 10 seconds
    if (job.execute(0)) return; // one job has been executed
  }
 

Я не жду запущенной задачи и просто выполняю первую доступную в течение 10 секунд, и я делаю это как можно быстрее, это нормально.
Тем не менее, я здесь активно жду, что, как известно, ужасно для общих выступлений, и этого следует избегать.

Моя третья попытка:

 for (Job job: jobs) {
  if (job.execute(10000/jobs.size())) return; // one job has been executed
}
 

Теперь я жду не более 10 секунд по желанию. Но это не самое оптимальное решение.
Допустим, у меня есть 10 рабочих мест, 9 — е-бесплатное, в то время как первые 8 остаются на работе. В этом случае я все равно буду бесполезно ждать 8 секунд, прежде чем найду свободный и выполню его.

Можем ли мы сделать лучше ?

В качестве предпочтения я хотел бы стандартное решение Java 11 без внешней зависимости. Класс заданий в моем примере находится под моим контролем, поэтому при необходимости я могу его реорганизовать.

Спасибо вам за ваши ответы.

Комментарии:

1. Я не читал все это прямо сейчас, но вы взглянули на ThreadPoolExecutor или, если это не соответствует приоритетной очереди и всплывающим заданиям, которые были запущены из этой очереди и при необходимости в отдельный список?

2. @Thomas: Короче говоря, в реальном случае задание представляет собой подготовленное заявление JDBC, и я хочу выполнить запрос (с результатом) при первом незанятом соединении. На самом деле, я уже нахожусь внутри задачи ThreadPoolExecutor при выполнении запроса, так что нет, я не хочу запускать ее в другом потоке и не хочу запускать несколько раз один и тот же запрос для всех подключений параллельно.

3. Не должны ли пулы соединений уже предоставлять эту функциональность, т. Е. Просто выбрать следующий запрос, захватить соединение из пула и выполнить, если оно у вас появится.

4. Почему бы не использовать а BlockingQueue<Job> вместо а List<Job> ? Очередь может содержать все задания, которые готовы к запуску. Если поток готов выполнить задание, он может poll(10, TimeUnit.SECONDS) выполнить его для следующего задания (тем самым удаляя его из очереди, «блокируя» его для других потоков). После завершения задания он может offer(job) снова поместить его в очередь, тем самым сделав его доступным для других потоков.

Ответ №1:

Что вам нужно, так это попытаться получить блокировку в течение 10 секунд из N разных потоков. Если вам это удастся, поднимите флаг (вам нужно сделать это потокобезопасно), и если флаг уже поднят, снимите текущую блокировку, в противном случае приступайте к выполнению задачи.

PS: N это количество рабочих мест.

Комментарии:

1. Не слишком ли опасно отправлять N задач в один и тот же ThreadPoolExecutor, в котором я уже работаю (возможный тупик) ? Из N задач, запущенных параллельно, как мне определить, какая из них на самом деле завершилась с результатом (я чувствую, что у меня точно такая же проблема, как знать, как ждать нескольких будущих, так же, как и с блокировками) Подходит ли AtomicBoolean для флага ? Спасибо вам за ваш ответ.

2. Вам нужно использовать другой набор потоков. Вы можете записать, какое задание было фактически выполнено. AtomicBoolean это хороший выбор для флага.

3. Я надеялся, что есть что-то попроще, но, похоже, это не так. Спасибо, ответ принят.