Статьи

Учебник по параллелизму Java — Семафоры

Это первая часть серии статей о параллелизме Java. В частности, мы собираемся углубиться в инструменты параллелизма, встроенные в Java 1.5 и выше. Мы предполагаем, что у вас есть базовые знания по синхронизации и изменчивым ключевым словам.

Первый пост будет посвящен семафорам, особенно по семафорам . Семафоры часто неправильно понимают и недостаточно используют для ограничения доступа к ресурсам. Они игнорируются для других способов управления доступом к ресурсам. Но семафоры дают нам набор инструментов, который выходит за рамки того, что могут дать нам обычная синхронизация и другие инструменты.

Так что такое семафор? Самый простой способ представить семафор — это рассмотреть его как абстракцию, которая позволяет получать n единиц и предлагает механизмы получения и выпуска. Это безопасно позволяет нам гарантировать, что только n процессов могут получить доступ к определенному ресурсу в данный момент времени .

Это все хорошо, но какой цели это послужит? Ну, вот один пример, который поможет объяснить его использование. Он использует хорошо разработанный класс Semaphore, представленный в 1.5, расположенный в пакете java.util.concurrent.

Ограничение соединений

Возможно, у нас есть процесс, который периодически загружает нам ресурсы через HTTP. Мы не хотим спамить ни одного хоста и в то же время хотим ограничить количество создаваемых нами соединений, чтобы не исчерпывать ограниченные дескрипторы файлов или исходящие соединения, которые нам разрешены. Простой способ сделать это был бы с семафором:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConnectionLimiter {
   private final Semaphore semaphore;
 
   private ConnectionLimiter(int maxConcurrentRequests) {
       semaphore = new Semaphore(maxConcurrentRequests);
   }
 
   public URLConnection acquire(URL url) throws InterruptedException,
                                                IOException {
       semaphore.acquire();
       return url.openConnection();
   }
 
   public void release(URLConnection conn) {
       try {
           /*
           * ... clean up here
           */
       } finally {
           semaphore.release();
       }
   }
}

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

Опасности

Как и в большинстве методов блокировки или синхронизации, существуют некоторые потенциальные проблемы.

Помните номер один: всегда высвобождайте то, что приобретаете . Это делается с помощью конструкций try..finally.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws Exception {
   Semaphore s1 = new Semaphore(1);
   Semaphore s2 = new Semaphore(1);
 
   Thread t = new Thread(new DoubleResourceGrabber(s1, s2));
   // now reverse them ... here comes trouble!
   Thread t2 = new Thread(new DoubleResourceGrabber(s2, s1));
 
   t.start();
   t2.start();
 
   t.join();
   t2.join();
   System.out.println("We got lucky!");
}
 
private static class DoubleResourceGrabber implements Runnable {
   private Semaphore first;
   private Semaphore second;
 
   public DoubleResourceGrabber(Semaphore s1, Semaphore s2) {
       first = s1;
       second = s2;
   }
 
   public void run() {
       try {
           Thread t = Thread.currentThread();
 
           first.acquire();
           System.out.println(t + " acquired " + first);
 
           Thread.sleep(200); // demonstrate deadlock
 
           second.acquire();
           System.out.println(t + " acquired " + second);
 
           second.release();
           System.out.println(t + " released " + second);
 
           first.release();
           System.out.println(t + " released " + first);
       } catch (InterruptedException ex) {
           ex.printStackTrace();
       }
   }
}

Если вы запустите это, у вас, скорее всего, будет зависший процесс. Проблемы порядка блокировки относятся к семафорам так же, как к обычным мьютексам или синхронизации в Java. В некоторых случаях тайм-ауты (см. Примечание к tryAcquire () далее в статье) можно использовать для предотвращения зависаний процесса, приводящих к зависанию процесса, но обычно тупик является признаком логической ошибки, которой можно избежать. Если вы не знакомы с тупиками, я рекомендую вам ознакомиться с ними. В Википедии есть достойная статья о взаимоблокировках, которая в равной степени относится ко всем языкам.

Основные вещи, с которыми вам следует быть осторожными при использовании семафоров (включая двоичные семафоры, т.е. мьютексы):

  • Не освобождает после получения (пропущенный вызов освобождения или исключение, и нет блока finally)
  • Долго держал семафоры, вызывая голодную нить
  • Тупики (как видно выше)

Полезные трюки с семафорами

Одним интересным свойством семафоров в Java является то, что выпуск не должен вызываться тем же потоком, что и acqu . Это означает, что вы можете иметь ограничитель потоков, который объединяет или создает потоки на основе семафора, вызывая acqu (). Затем работающий поток может освободить свое собственное разрешение семафора после его завершения. Это полезное свойство, которого нет у обычных мьютексов в Java.

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

Наконец, есть несколько полезных методов, с которыми можно ознакомиться в семафоре Java. Метод acquInterruptibly () получит ресурс, повторная попытка , если он будет прерван. Это означает отсутствие внешней обработки InterruptedException. Метод tryAcquire () позволяет нам ограничить время ожидания разрешения — мы можем либо немедленно вернуться, если нет разрешения на получение, либо подождать указанное время ожидания. Если у вас есть какие-то взаимные блокировки, которые вы не можете легко исправить или отследить, вы можете предотвратить блокировку процессов, используя tryAcquire () с подходящими таймаутами.

Пользы

Каковы возможные варианты подсчета семафоров? На ум приходят следующие:

  • Ограничение одновременного доступа к диску (это может снизить производительность из-за конкурирующих запросов к диску)
  • Ограничение создания потока
  • Пул соединений JDBC / ограничение
  • Регулирование сетевого подключения
  • Регулирование загрузки процессора или памяти

Конечно, семафор — это довольно низкоуровневый строительный блок для контроля доступа и синхронизации. Java предоставляет нам множество механизмов и стратегий параллелизма, которые были представлены в Java 1.5 и более поздних версиях. В следующих статьях мы расскажем о некоторых более абстрактных методах управления параллелизмом, включая Executors, BlockingQueues, Barriers, Futures, а также некоторые новые параллельные классы Collection.

Какое использование вы нашли для семафоров? Дайте нам знать, оставив комментарий — мы любим говорить программное обеспечение.

Ссылка: Java Concurrency Part 1 — Семафоры от наших партнеров JCG в блоге Carfey Software .

Статьи по Теме :
Связанные фрагменты: