Използване на Mutex обект в Java

1. Общ преглед

В този урок ще видим различни начини за внедряване на мутекс в Java .

2. Мютекс

В многонишково приложение може да се наложи две или повече нишки да имат достъп до споделен ресурс едновременно, което води до неочаквано поведение. Примери за такива споделени ресурси са структури от данни, устройства за въвеждане и извеждане, файлове и мрежови връзки.

Ние наричаме този сценарий състояние на състезанието . И частта от програмата, която осъществява достъп до споделения ресурс, е известна като критичната секция . Така че, за да избегнем състезателно състояние, трябва да синхронизираме достъпа до критичната секция.

Mutex (или взаимно изключване) е най-простият тип синхронизатор - той гарантира, че само една нишка може да изпълнява критичната секция на компютърна програма наведнъж .

За достъп до критична секция, нишка придобива mutex, след това осъществява достъп до критичната секция и накрая освобождава mutex. Междувременно всички останали нишки блокират, докато мютексът се освободи. Веднага след като нишка излезе от критичната секция, друга нишка може да влезе в критичната секция.

3. Защо Mutex?

Първо, нека вземем пример за клас SequenceGeneraror , който генерира следващата последователност чрез увеличаване на currentValue с единица всеки път:

public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }

Сега, нека създадем тестов случай, за да видим как се държи този метод, когато множество нишки се опитват да получат достъп до него едновременно:

@Test public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception { int count = 1000; Set uniqueSequences = getUniqueSequences(new SequenceGenerator(), count); Assert.assertEquals(count, uniqueSequences.size()); } private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Set uniqueSequences = new LinkedHashSet(); List
    
      futures = new ArrayList(); for (int i = 0; i < count; i++) { futures.add(executor.submit(generator::getNextSequence)); } for (Future future : futures) { uniqueSequences.add(future.get()); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdown(); return uniqueSequences; }
    

След като изпълним този тестов случай, можем да видим, че той се проваля през повечето време с причината, подобна на:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)

На uniqueSequences е трябвало да имат размер, равен на броя пъти, ние сме изпълнени с getNextSequence метод в нашия тест случай. Това обаче не е така поради състоянието на състезанието. Очевидно не искаме това поведение.

Така че, за да избегнем такива състезателни условия, трябва да се уверим, че само една нишка може да изпълнява метода getNextSequence наведнъж . В такива сценарии можем да използваме мютекс за синхронизиране на нишките.

Има различни начини, можем да внедрим мютекс в Java. И така, по-нататък ще видим различните начини за внедряване на мютекс за нашия клас SequenceGenerator .

4. Използване на синхронизирана ключова дума

Първо ще обсъдим синхронизираната ключова дума, която е най-простият начин за внедряване на мютекс в Java.

Всеки обект в Java има присъщо заключване, свързано с него. В синхрон метода и на синхронизирани блока използвате тази присъща за заключване , за да ограничи достъпа на критичния участък до само една нишка в даден момент.

Следователно, когато нишка извика синхронизиран метод или влезе в синхронизиран блок, тя автоматично придобива заключването. Заключването се освобождава, когато методът или блокът завършат или се изведе изключение от тях.

Нека променим getNextSequence, за да има мютекс , просто като добавим синхронизираната ключова дума:

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }

В синхронизирано блок е подобен на синхронизиран метод, с по-голям контрол над критичното сечение и обекта може да се използва за заключване.

И така, нека сега видим как можем да използваме синхронизирания блок за синхронизиране на персонализиран mutex обект :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }

5. Използване на ReentrantLock

Класът ReentrantLock е въведен в Java 1.5. Той осигурява по-голяма гъвкавост и контрол от подхода на синхронизираните ключови думи.

Нека да видим как можем да използваме ReentrantLock за постигане на взаимно изключване:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator { private ReentrantLock mutex = new ReentrantLock(); @Override public int getNextSequence() { try { mutex.lock(); return super.getNextSequence(); } finally { mutex.unlock(); } } }

6. Използване на Semaphore

Подобно на ReentrantLock , класът Semaphore също е въведен в Java 1.5.

Докато в случай на мутекс само една нишка може да има достъп до критична секция, Semaphore позволява фиксиран брой нишки за достъп до критична секция . Следователно, ние също можем да реализираме мютекс, като зададем броя на разрешените нишки в Семафор на един .

Нека сега създадем друга безопасна за нишки версия на SequenceGenerator, използвайки Semaphore :

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator { private Semaphore mutex = new Semaphore(1); @Override public int getNextSequence() { try { mutex.acquire(); return super.getNextSequence(); } catch (InterruptedException e) { // exception handling code } finally { mutex.release(); } } }

7. Използване на мониторния клас на Guava

Досега видяхме опциите за внедряване на mutex, използвайки функции, предоставени от Java.

Въпреки това, Монитор клас библиотека Guava на Google е по-добра алтернатива на ReentrantLock клас. Според неговата документация кодът, използващ Monitor, е по-четим и по-малко податлив на грешки от кода, използващ ReentrantLock .

Първо ще добавим зависимостта Maven за Guava:

 com.google.guava guava 28.0-jre 

Сега ще напишем друг подклас на SequenceGenerator, използвайки класа Monitor :

public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }

8. Заключение

В този урок разгледахме концепцията за мютекс. Също така видяхме различните начини да го внедрим в Java.

Както винаги, пълният изходен код на примерите за кодове, използвани в този урок, е достъпен в GitHub.