В параллельном программировании семафор — это вспомогательное средство синхронизации, которое управляет доступом к общему ресурсу несколькими потоками. Он поддерживает набор разрешений, которые потоки должны получить перед доступом к ресурсам, таким как базы данных, файловые системы или сетевые соединения.
В предыдущем уроке мы узнали о бинарных семафорах, которые можно использовать для управления доступом к одной копии ресурса с использованием значения счетчика 0 или 1. Однако семафоры также можно использовать, когда нужно защитить различные копии ресурса, которые могут выполняться более чем одним потоком одновременно. В этом примере мы узнаем, как использовать семафор для защиты более чем одной копии ресурса.
Прежде чем двигаться дальше, давайте вернемся к концепции семафора.
1. Как работает семафор?
Семафор можно визуализировать как счетчик, который можно увеличивать или уменьшать. Вы инициализируете семафор числом, например 5.
- Этот семафор можно уменьшать максимум пять раз подряд, пока счетчик не достигнет 0.
- Как только счетчик станет равен нулю, вы можете увеличивать его до пяти раз, чтобы довести его до 5.
- Значение счетчика семафора ДОЛЖНО всегда находиться в пределах 0 = 5(в нашем случае).
Очевидно, что семафоры — это больше, чем просто счетчики. Они могут заставить потоки ждать, когда значение счетчика равно нулю, т. е. они действуют как Locks с функциональностью счетчика.
Что касается многопоточности, когда поток хочет получить доступ к одному из общих ресурсов(охраняемых семафором), он должен сначала получить семафор.
Если внутренний счетчик семафора больше 0, семафор уменьшает счетчик и разрешает доступ к общему ресурсу. В противном случае, если счетчик семафора равен 0, семафор переводит поток в спящий режим до тех пор, пока счетчик не станет больше 0. Значение 0 в счетчике означает, что другие потоки используют все общие ресурсы, поэтому поток, который хочет использовать один из них, должен ждать, пока один из них не освободится.
Поток может продолжить работу только в том случае, если он успешно получил разрешение. Когда поток завершается, он должен освободить разрешение.
Подробнее: Как использовать блокировки в Java
2. Как использовать семафор?
Чтобы продемонстрировать концепцию, мы будем использовать семафор для управления тремя принтерами, которые могут печатать несколько документов одновременно.
2.1. Печать задания
Этот класс представляет независимое задание печати, которое может быть отправлено в очередь принтера. А из очереди его может взять любой принтер и выполнить задание печати. Этот класс реализует интерфейс Runnable, так что принтер может выполнить его, когда придет его очередь.
class PrintingJob implements Runnable {private PrinterQueue printerQueue;public PrintingJob(PrinterQueue printerQueue) {this.printerQueue = printerQueue;}@Overridepublic void run() {System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName());printerQueue.printJob(new Object());}}
2.2.Очередь печати
Класс PrinterQueue представляет очередь печати/принтер. Этот класс имеет 3 основных атрибута, которые управляют логикой выбора свободного принтера из 3 принтеров и блокируют его для печати задания. После печати документа принтер освобождается, так что он снова свободен и доступен для печати нового задания из очереди печати.
Этот класс имеет два метода getPrinter() и releasePrinter(), которые отвечают за получение свободного принтера и помещение его обратно в пул свободных принтеров.
Другой метод printJob() фактически выполняет основную работу, то есть получает принтер, выполняет задание на печать и затем освобождает принтер.
Для выполнения работы используются две переменные:
- семафор: эта переменная отслеживает количество принтеров, используемых в любой момент времени.
- printerLock: используется для блокировки пула принтеров перед проверкой/получением свободного принтера из трех доступных принтеров.
class PrinterQueue {//This Semaphore will keep track of no. of printers used at any point of time.private final Semaphore semaphore;//While checking/acquiring a free printer out of three available printers, we will use this lock.private final Lock printerLock;//This array represents the pool of free printers.private boolean freePrinters[];public PrinterQueue() {semaphore = new Semaphore(3);freePrinters = new boolean[3];Arrays.fill(freePrinters, true);printerLock = new ReentrantLock();}public void printJob(Object document) {try {//Decrease the semaphore counter to mark a printer busysemaphore.acquire();//Get the free printerint assignedPrinter = getPrinter();//Print the jobLong duration =(long)(Math.random() * 10000);System.out.println(Thread.currentThread().getName()+ ": Printer " + assignedPrinter+ " : Printing a Job during " +(duration / 1000)+ " seconds :: Time - " + new Date());Thread.sleep(duration);//Printing is done; Free the printer to be used by other threads.releasePrinter(assignedPrinter);}catch(InterruptedException e) {e.printStackTrace();}finally {System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());//Increase the semaphore counter backsemaphore.release();}}//Acquire a free printer for printing a jobprivate int getPrinter() {int foundPrinter = -1;try {//Get a lock here so that only one thread can go beyond this at a timeprinterLock.lock();//Check which printer is freefor(int i = 0; i < freePrinters.length; i++){//If free printer found then mark it busyif(freePrinters[i]){foundPrinter = i;freePrinters[i] = false;break;}}}catch(Exception e) {e.printStackTrace();} finally {//Allow other threads to check for free priniterprinterLock.unlock();}return foundPrinter;}//Release the printerprivate void releasePrinter(int i) {printerLock.lock();//Mark the printer freefreePrinters[i] = true;printerLock.unlock();}}
Подробнее: Как использовать двоичный семафор?
2.3.Демо
Давайте проверим нашу программу принтера:
public class SemaphoreExample{public static void main(String[] args){PrinterQueue printerQueue = new PrinterQueue();Thread thread[] = new Thread[10];for(int i = 0; i < 10; i++){thread[i] = new Thread(new PrintingJob(printerQueue), "Thread " + i);}for(int i = 0; i < 10; i++){thread[i].start();}}}
В приведенном выше примере объект создается с использованием 3 в качестве параметра конструктора. Первые три потока, которые вызовут метод acquire(), получат доступ к принтерам, а остальные будут заблокированы. Когда поток завершит критическую секцию и освободит семафор, другой поток получит его.
В методе printJob() поток получает индекс принтера, назначенного для печати этого задания.
Вывод программы:
Thread 1: Going to print a documentThread 4: Going to print a documentThread 9: Going to print a documentThread 8: Going to print a documentThread 6: Going to print a documentThread 7: Going to print a documentThread 2: Going to print a documentThread 5: Going to print a documentThread 3: Going to print a documentThread 0: Going to print a documentThread 9: PrintQueue 2 : Printing a Job during 2 seconds :: Time - Tue Jan 13 16:28:58 IST 2015Thread 4: PrintQueue 1 : Printing a Job during 7 seconds :: Time - Tue Jan 13 16:28:58 IST 2015Thread 1: PrintQueue 0 : Printing a Job during 1 seconds :: Time - Tue Jan 13 16:28:58 IST 2015Thread 1: The document has been printedThread 8: PrintQueue 0 : Printing a Job during 1 seconds :: Time - Tue Jan 13 16:29:00 IST 2015Thread 9: The document has been printedThread 6: PrintQueue 2 : Printing a Job during 0 seconds :: Time - Tue Jan 13 16:29:01 IST 2015Thread 6: The document has been printedThread 7: PrintQueue 2 : Printing a Job during 4 seconds :: Time - Tue Jan 13 16:29:01 IST 2015Thread 8: The document has been printedThread 2: PrintQueue 0 : Printing a Job during 5 seconds :: Time - Tue Jan 13 16:29:02 IST 2015Thread 7: The document has been printedThread 5: PrintQueue 2 : Printing a Job during 8 seconds :: Time - Tue Jan 13 16:29:05 IST 2015Thread 4: The document has been printedThread 3: PrintQueue 1 : Printing a Job during 4 seconds :: Time - Tue Jan 13 16:29:06 IST 2015Thread 2: The document has been printedThread 0: PrintQueue 0 : Printing a Job during 4 seconds :: Time - Tue Jan 13 16:29:08 IST 2015Thread 3: The document has been printedThread 0: The document has been printedThread 5: The document has been printed
Вот и все по этой простой, но важной концепции. Пишите мне ваши вопросы и комментарии, если таковые имеются.