* Монитори и семафори в Java
Публикувано на 04 юли 2023 в раздел ПИК3 Java.
Вече съм показвал няколко примери с употребата на монитори. Зад кадър в тях стои т.нар. мютекс (mutex) - представете си го като ключалка (променлива със стойности „отключено“ и „заключено“). Идеята е, че когато някоя нишка заключи мютекса, други могат да видят това и съответно да спрат и да изчакат, докато тя си свърши работата. Мониторите автоматизират този процес, като организират централизирано управлявана опашка за изчакване на ресурсите. Това се прави чрез синхронизирани блокове или синхронизирани методи. Тук просто ще припомня с елементарен пример.
Нека имаме прост брояч и две нишки, които конкурентно една на друга го увеличават. Първоначално ще е без синхронизация, т.е. очаквано ще има т.нар. race condition:
private static Integer count = 0; public static void main(String[] args) throws InterruptedException { Thread incr1 = new Thread(){ public void run(){ for(int i=0; i<100000; i++){ count++; } } }; Thread incr2 = new Thread(){ public void run(){ for(int i=0; i<100000; i++){ count++; } } }; incr1.start(); incr2.start(); incr1.join(); incr2.join(); System.out.println(count); }
Очаквано накрая броячът няма да даде 200000, а по-малко число - някои операции за увеличение са се припокрили една друга, защото те не са атомарни. Заключването с монитор ще разреши този проблем и увеличаването на брояча ще бъде синхронизирано:
private static Object monitor = new Object(); private static Integer count = 0; public static void main(String[] args) throws InterruptedException { Thread incr1 = new Thread(){ public void run(){ synchronized(monitor){ for(int i=0; i<100000; i++){ count++; } } } }; Thread incr2 = new Thread(){ public void run(){ synchronized(monitor){ for(int i=0; i<100000; i++){ count++; } } } }; incr1.start(); incr2.start(); incr1.join(); incr2.join(); System.out.println(count); }
Реално първата нишка, която изпълни своя synchronized блок ще „заключи“ мютекса зад монитора и по този начин другата няма да може да продължи, докато той не бъде отключен - тя ще се нареди на опашка, за да чака реда си. В Java всеки обект има вграден монитор.
Семафорите от своя страна позволяват паралелна работа на повече от една нишка, но им слага горен лимит. Ако например лимитът е 5, това означава, че до пет нишки могат да работят паралелно, а ако дойде шеста - тя вече ще трябва да чака по подобие на случващото се при монитора. Може да приемете опростено мониторите като семафори с лимит 1.
Ще демонстрирам как се работи със семафори в Java чрез задача, в която нишките ще се борят за ограничен споделен ресурс, като този път ще използвам вграден клас java.util.concurrent.Semaphore (в предишни примери съм показвал подобна задача, но изцяло с ръчна синхронизация и употреба на wait() и notify(), което е изключително трудоемко и объркващо). Представете си, че няколко хора влизат в кафене. За съжаление има само два свободни стола за сядане. Идеята е първите двама, които стигнат до столовете, да седнат на тях, а останалите да ги изчакат на опашка. Когато някой стане, най-дълго чакалия ще седне на неговото място веднага. Ето примерна реализация на това:
public class Philosophers { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Client("Karl", "Marx")); Thread t2 = new Thread(new Client("Friedrich", "Nietzsche")); Thread t3 = new Thread(new Client("Immanuel", "Kant")); Thread t4 = new Thread(new Client("Muhammad", "Abduh")); Thread t5 = new Thread(new Client("Bertrand", "Russell")); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } } record Client(String firstname, String lastname) implements Runnable { private static Semaphore sem = new Semaphore(2); public void run() { try { System.out.println(firstname + " " + lastname + " отива към масата..."); sem.acquire(); System.out.println(firstname + " " + lastname + " седна на масата и започна да яде..."); Thread.sleep(3000); System.out.println(firstname + " " + lastname + " се наяде и освобождава масата!"); sem.release(); } catch (InterruptedException ex) { System.err.println("Нещо се обърка при " + firstname + " " + lastname); } } }
В получер шрифт е отбелязано как се дефинира семафора, как се заключва и как се отключва. Именно метод acquire() е блокиращ - ако в семафора няма свободни слотове за нишки, тази, в която е изпълнен, ще трябва да чака да се освободи.
Добави коментар