Первым сразу же распознаваемым среди объектов синхронизации является семафор, реализуемый в классе Semaphore.
Семафор управляет доступом к общему ресурсу с помощью счетчика. Если счетчик больше нуля, доступ разрешается,а если он равен нулю, то в доступе будет отказано.
В действительности этот счетчик подсчитывает разрешения, открывающие доступ к общему ресурсу. Следовательно,чтобы получить доступ к ресурсу, поток исполнения должен получить у семафора разрешение на доступ.
Как правило, поток исполнения, которому требуется доступ к общему ресурсу, и пытается получить разрешение, чтобы воспользоваться семафором. Если значение счетчика семафора окажется больше нуля, поток исполнения получит разрешение, после чего значение счетчика семафора уменьшается на единицу. В противном случае поток будет заблокирован до тех пор, пока он не сумеет получить разрешение.
Если потоку исполнения доступ к общему ресурсу больше не нужен,он освобождает разрешение, в результате чего значение счетчика семафора увеличивается на единицу. Если в это время другой поток исполнения ожидает разрешения, то он сразу же его получает. В Java этот механизм реализуется в классе Semaphore.
В классе Semaphore имеются два приведенных ниже конструктора:
1 2 |
Semaphore(int число); Semaphore(int число, boolean способ); |
Здесь параметр число обозначает исходное значение счетчика разрешений. Таким образом, параметр число определяет количество потоков исполнения, которым может быть одновременно предоставлен доступ к общему ресурсу.
Если параметр число принимает значение 1, к ресурсу может обратиться только один поток исполнения. По умолчанию ожидающим потокам исполнения предоставляется разрешение в неопределенном порядке.
Если же присвоить параметру способ логическое значение true, то тем самым можно гарантировать, что разрешения будут предоставляться ожидающим потокам исполнения в том порядке, в каком они запрашивали доступ.
Чтобы получить разрешение, достаточно вызвать метод acquire(), который имеет две формы:
1 2 |
void acquire() throws InterruptedException void acquire(int число) throws InterruptedException |
Первая форма запрашивает одно разрешение, а вторая число разрешений. Обычно используется первая форма.
Если разрешение не будет предоставлено вовремя вызова метода, то исполнение вызывающего потока будет приостановлено до тех пор, пока не будет получено разрешение.
Чтобы освободить разрешение, следует вызвать метод release(). Ниже приведены общие формы этого метода:
1 2 |
void release() void release(int число) |
В первой форме освобождается одно разрешение, а во второй — количество разрешений, обозначаемое параметром число.
Чтобы воспользоваться семафором для управления доступом к ресурсу, каждый поток исполнения, которому требуется этот ресурс, должен вызвать метод acquire(), прежде чем обращаться к ресурсу.
Когда поток исполнения завершает пользование ресурсом, он должен вызвать метод release(), чтобы освободить ресурс. В приведенном ниже примере программы демонстрируется применение семафора.
1 2 3 4 5 6 7 8 9 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
// Простой пример применения семафора в Java import java.util.concurrent.*; class SemDemo { public static void main(String args[]) { Semaphore sem = new Semaphore(1); new IncThread(sem, "A"); new DecThread(sem, "B"); } } // Общий ресурс class Shared { static int count = 0; } // Поток исполнения, увеличивающий значение счетчика на единицу class IncThread implements Runnable { String name; Semaphore sem; IncThread(Semaphore s, String n) { sem = s; name = n; new Thread(this).start(); } public void run() { System.out.println("Запуск потока " + name); try { // сначала получить разрешение System.out.println("Поток " + name + " ожидает разрешения"); sem.acquire(); System.out.println("Поток " + name + " получает разрешение"); // а теперь получить доступ к общему ресурсу for(int i = 0; i < 5; i++) { Shared.count++; System.out.println(name + ": " + Shared.count); // Разрешить, если возможно, переключение контекста Thread.sleep(10); } } catch (InterruptedException exc) { System.out.println(exc); } // освободить разрешение System.out.println("Поток " + name + " освобождает разрешение"); sem.release(); } } // Поток исполнения, уменьшающий значение счетчика на единицу class DecThread implements Runnable { String name; Semaphore sem; DecThread(Semaphore s, String n) { sem = s; name = n; new Thread(this).start(); } public void run() { System.out.println("Запуск потока " + name); try { // сначала получить разрешение System.out.println("Поток " + name + " ожидает разрешения"); sem.acquire(); System.out.println("Поток " + name + " получает разрешение"); // а теперь получить доступ к общему ресурсу for(int i = 0; i < 5; i++) { Shared.count--; System.out.println(name + ": " + Shared.count); // Разрешить, если возможно, переключение контекста Thread.sleep(10); } } catch (InterruptedException exc) { System.out.println(exc); } // освободить разрешение System.out.println("Поток " + name + " освобождает разрешение"); sem.release(); } } |
Ниже приведен примерный результат выполнения данной программы. (Конкретный порядок следования потоков исполнения может быть иным.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Запуск потока A Запуск потока B Поток B ожидает разрешения Поток A ожидает разрешения Поток B получает разрешение B: -1 B: -2 B: -3 B: -4 B: -5 Поток B освобождает разрешение Поток A получает разрешение A: -4 A: -3 A: -2 A: -1 A: 0 Поток A освобождает разрешение |
Для управления доступом к переменной count, которая является статической переменной класса Shared, в данной программе используется семафор. Значение переменной Shared.count увеличивается на 5 в методе run() из класса IncThread и уменьшается на 5 в одноименном методе из класса DecThread.
Для защиты потоков исполнения, представленных этими двумя классами, от одновременного доступа к переменной Shared.count такой доступ предоставляется только после того, как будет получено разрешение от управляющего семафора.
По завершении доступа к данной переменной как к общему ресурсу разрешение на него освобождается. Таким образом, только один поток исполнения может одновременно получить доступ к переменной Shared.count, что и подтверждают результаты выполнения данной программы.
Обратите внимание на то, что в методе run() из классов IncThread и DecThread вызывается метод sleep(). Он гарантирует, что доступ к переменной Shared.count будет синхронизироваться семафором.
В частности, вызов метода sleep() из метода run() приводит к тому, что вызывающий поток исполнения будет приостанавливаться в промежутках между последовательными попытками доступа к переменной Shared.count. Это, как правило, позволяет исполняться второму потоку.
Но благодаря семафору второй поток исполнения должен ожидать до тех пор, пока первый поток исполнения не освободит разрешение. А это произойдет только после того, как будут завершены все попытки доступа со стороны первого потока исполнения.
Таким образом, значение переменной Shared.count сначала увеличивается на 5 в объекте класса IncThread, а затем уменьшается на 5 в объекте класса DecThread. Увеличение и уменьшение значения этой переменной происходит строго по порядку.
Если бы в данном примере не использовался семафор, то попытки доступа к переменной Shared.count, производимые каждым потоком исполнения, осуществлялись бы одновременно, поэтому увеличение и уменьшение значения этой переменной происходило бы не по порядку.
Чтобы убедиться в этом, попробуйте закомментировать вызовы методов acquire() и release(). Запустив данную программу на выполнение, вы обнаружите, что доступ к переменной Shared.count больше не является синхронизированным, и каждый поток исполнения обращается к переменной Shared.count, как только для него выделяется временной интервал.
Несмотря на то что применение семафора, как правило, не представляет особой сложности, как демонстрируется в предыдущем примере программы, возможны и более сложные варианты его применения.
Ниже приведен один из таких примеров. Он представляет собой переработанную версию программы, реализующей функции поставщика и потребителя . В данном варианте используются два семафора, регулирующие потоки исполнения поставщика и потребителя и гарантирующие, что после каждого вызова метода put() будет следовать соответствующий вызов метода get().
1 2 3 4 5 6 7 8 9 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
// Реализация поставщика и потребителя, использующая // семафоры для управления синхронизацией import java.util.concurrent.Semaphore; class Q { int n; // начать с недоступного семафора потребителя static Semaphore semCon = new Semaphore(0); static Semaphore semProd = new Semaphore(1); void get() { try { semCon.acquire(); } catch(InterruptedException e) { System.out.println("Перехвачено исключение типа InterruptedException"); } System.out.println("Получено: " + n); semProd.release(); } void put(int n) { try { semProd.acquire(); } catch(InterruptedException e) { System.out.println("Перехвачено исключение типа InterruptedException"); } this.n = n; System.out.println("Отправлено: " + n); semCon.release(); } } class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { for(int i = 0; i < 7; i++) { q.put(i); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { for(int i = 0; i < 7; i++) { q.get(); } } } class ProdCon { public static void main(String args[]) { Q q = new Q(); new Consumer(q); new Producer(q); } } |
Ниже показана результат, выводимый данной программой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Отправлено: 0 Получено: 0 Отправлено: 1 Получено: 1 Отправлено: 2 Получено: 2 Отправлено: 3 Получено: 3 Отправлено: 4 Получено: 4 Отправлено: 5 Получено: 5 Отправлено: 6 Получено: 6 |
Как видите, в данном примере синхронизируются вызовы методов put() и get(). Это означает, что после каждого вызова метода put() следует вызов метода get() и поэтому ни одно значение не может быть пропущено.
Если бы не семафоры, вызовы метода put() могли бы происходить несогласованно с вызовами метода get(), что привело бы к пропуску некоторых значений. (Чтобы убедиться в этом,удалите код семафора из данного примера и посмотрите полученные результаты.)
Надлежащая последовательность вызовов методов put() и get() соблюдается двумя семафорами: semProd и semCon.
Прежде чем метод put() сможет предоставить значение, он должен получить разрешение от семафора semProd. Установив значение, он освобождает семафор semProd.
Прежде чем метод get() сможет употребить значение, он должен получить разрешение от семафора semCon.Употребив значение, он освобождает семафор semCon. Такой механизм передачи и получения значений гарантирует, что после каждого вызова метода put() будет следовать вызов метода get().
Обратите внимание на то, что семафор semCon инициализируется без доступных разрешений. Этим гарантируется, что метод put() выполняется первым. Возможность задавать исходное состояние синхронизации является одной из самых сильных сторон семафоров.