Привет!
Изучая многопоточность на JavaRush, ты часто встречал понятия «мьютекс» и «монитор». Сможешь сейчас, без подглядывания ответить, чем они отличаются? :)
Если смог — молодец! Если же нет (а чаще всего так и бывает) — неудивительно.
Понятия «мьютекс» и «монитор» действительно связаны между собой. Более того, читая лекции и смотря видео по многопоточности на внешних ресурсах в Интернете, ты столкнешься с еще одним похожим понятием — «семафор».
Его функционал тоже во многом схож с монитором и мьютексом. Поэтому разберемся с этими тремя терминами, рассмотрим несколько примеров и окончательно упорядочим в голове понимание того, чем же они друг от друга отличаются :)
Мьютекс
Мьютекс — это специальный объект для синхронизации потоков. Он «прикреплен» к каждому объекту в Java — это ты уже знаешь :) Неважно, пользуешься ли ты стандартными классами или создал собственные классы, скажем,Cat
и Dog
: у всех объектов всех классов есть мьютекс.
Название «мьютекс» происходит от английского «MUTual EXclusion» — «взаимное исключение», и это отлично отражает его предназначение.
Как мы и говорили в одной из прошлых лекций, задача мьютекса — обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока.
Популярной аналогией мьютекса в реальной жизни можно считать «пример с туалетом».
Когда человек заходит в туалет, он закрывает изнутри дверь на замок.
Туалет выполняет роль объекта, доступ к которому получают несколько потоков. Замок на двери туалета — роль мьютекса, а очередь из людей снаружи — роль потоков.
Замок на двери — мьютекс туалета: он гарантирует, что внутри одновременно может находиться только один человек.
Иными словами, только один поток в определенное время может работать с общими ресурсами. Попытки других потоков (людей) получить доступ к занятым ресурсам будут неудачными.
У мьютекса есть несколько важных особенностей.
Во-первых, возможны только два состояния — «свободен» и «занят».
Это упрощает понимание принципа работы: можно провести параллели с булевыми переменными true/false или двоичной системой счисления 1/0.
Во-вторых, состояниями нельзя управлять напрямую. В Java нет механизмов, которые позволили бы явно взять объект, получить его мьютекс и присвоить ему нужный статус. Иными словами, ты не можешь сделать что-то типа:
Object myObject = new Object();
Mutex mutex = myObject.getMutex();
mutex.free();
Таким образом освободить мьютекс объекта нельзя. Прямой доступ к нему есть только у Java-машины. Программисты же работают с мьютексами с помощью средств языка.
Монитор
Монитор — это дополнительная «надстройка» над мьютексом. Фактически монитор — это «невидимый» для программиста кусок кода. Говоря о мьютексе ранее, мы приводили простой пример:
public class Main {
private Object obj = new Object();
public void doSomething() {
//...какая-то логика, доступная для всех потоков
synchronized (obj) {
//логика, которая одновременно доступна только для одного потока
}
}
}
В блоке кода, который помечен словом synchronized
, происходит захват мьютекса нашего объекта obj
.
Хорошо, захват-то происходит, но как именно обеспечивается «защитный механизм»? Почему при виде слова synchronized
остальные потоки не могут пройти внутрь блока?
Защитный механизм создает именно монитор! Компилятор преобразует слово synchronized
в несколько специальных кусков кода.
Еще раз вернемся к нашему примеру с методом doSomething()
и дополним его:
public class Main {
private Object obj = new Object();
public void doSomething() {
//...какая-то логика, доступная для всех потоков
//логика, которая одновременно доступна только для одного потока
synchronized (obj) {
/*выполнить важную работу, при которой доступ к объекту
должен быть только у одного потока*/
obj.someImportantMethod();
}
}
}
Вот что будет происходить «под капотом» нашей программы после того, как компилятор преобразует этот код:
public class Main {
private Object obj = new Object();
public void doSomething() throws InterruptedException {
//...какая-то логика, доступная для всех потоков
//логика, которая одновременно доступна только для одного потока:
/*до тех пор, пока мьютекс объекта занят -
любой другой поток (кроме того, который его захватил), спит*/
while (obj.getMutex().isBusy()) {
Thread.sleep(1);
}
//пометить мьютекс объекта как занятый
obj.getMutex().isBusy() = true;
/*выполнить важную работу, при которой доступ к объекту
должен быть только у одного потока*/
obj.someImportantMethod();
//освободить мьютекс объекта
obj.getMutex().isBusy() = false;
}
}
Пример, конечно, ненастоящий. Здесь мы с помощью Java-подобного кода попытались отразить то, что происходит в этот момент внутри Java-машины. Однако этот псевдокод дает отличное понимание того, что на самом деле происходит с объектом и потоками внутри блока synchronized
и как компилятор преобразует это слово в несколько «невидимых» для программиста команд.
По сути, монитор в Java выражен с помощью слова synchronized
. Весь код, который появился вместо слова synchronized
в последнем примере, — это и есть монитор.
Семафор
Еще одно слово, с которым ты сталкиваешься при самостоятельном изучении многопоточности — «семафор». Давай разберемся что это такое, и чем он отличается от монитора и мьютекса. Семафор — это средство для синхронизации доступа к какому-то ресурсу. Его особенность заключается в том, что при создании механизма синхронизации он использует счетчик. Счетчик указывает нам, сколько потоков одновременно могут получать доступ к общему ресурсу. Семафоры в Java представлены классомSemaphore
.
При создании объектов-семафоров мы можем использовать такие конструкторы:
Semaphore(int permits)
Semaphore(int permits, boolean fair)
В конструктор мы передаем:
int permits
— начальное и максимальное значение счетчика. То есть то, сколько потоков одновременно могут иметь доступ к общему ресурсу;boolean fair
— для установления порядка, в котором потоки будут получать доступ. Еслиfair
= true, доступ предоставляется ожидающим потокам в том порядке, в котором они его запрашивали. Если же он равен false, порядок будет определять планировщик потоков.
Мы немного упростим ее условия, для лучшего понимания.
Представь, что у нас есть 5 философов, которым нужно пообедать.
При этом у нас есть один стол, и одновременно находиться за ним могут не более двух человек.
Наша задача — накормить всех философов. Никто из них не должен остаться голодным, и при этом они не должны «заблокировать» друг друга при попытке сесть за стол (мы должны избежать deadlock).
Вот как будет выглядеть наш класс философа:
class Philosopher extends Thread {
private Semaphore sem;
// поел ли философ
private boolean full = false;
private String name;
Philosopher(Semaphore sem, String name) {
this.sem=sem;
this.name=name;
}
public void run()
{
try
{
// если философ еще не ел
if (!full) {
//Запрашиваем у семафора разрешение на выполнение
sem.acquire();
System.out.println (name + " садится за стол");
// философ ест
sleep(300);
full = true;
System.out.println (name + " поел! Он выходит из-за стола");
sem.release();
// философ ушел, освободив место другим
sleep(300);
}
}
catch(InterruptedException e) {
System.out.println ("Что-то пошло не так!");
}
}
}
А вот код для запуска нашей программы:
public class Main {
public static void main(String[] args) {
Semaphore sem = new Semaphore(2);
new Philosopher(sem,"Сократ").start();
new Philosopher(sem,"Платон").start();
new Philosopher(sem,"Аристотель").start();
new Philosopher(sem,"Фалес").start();
new Philosopher(sem,"Пифагор").start();
}
}
Мы создали семафор со счетчиком 2, чтобы соответствовать условию: одновременно есть могут только два философа. То есть, одновременно работать могут только два потока, ведь наш класс Philosopher
унаследован от Thread
!
Методы acquire()
и release()
класса Semaphore
управляют его счетчиком разрешений. Метод acquire()
запрашивает разрешение на доступ к ресурсу у семафора. Если счетчик > 0, разрешение предоставляется, а счетчик уменьшается на 1.
Метод release()
«освобождает» выданное ранее разрешение и возвращает его в счетчик (увеличивает счетчик разрешений семафора на 1).
Что же у нас получится при запуске программы? Решена ли задача, не передерутся ли наши философы, ожидая своей очереди? :)
Вот какой вывод в консоль мы получили:
Сократ садится за стол
Платон садится за стол
Сократ поел! Он выходит из-за стола
Платон поел! Он выходит из-за стола
Аристотель садится за стол
Пифагор садится за стол
Аристотель поел! Он выходит из-за стола
Пифагор поел! Он выходит из-за стола
Фалес садится за стол
Фалес поел! Он выходит из-за стола
У нас все получилось! И хотя Фалесу пришлось обедать в одиночку, думаю, он на нас не в обиде :)
Ты мог заметить некоторое сходство между мьютексом и семафором.
У них, в общем-то, одинаковое предназначение: синхронизировать доступ к какому-то ресурсу.
Разница только в том, что мьютекс объекта может захватить одновременно только один поток, а в случае с семафором используется счетчик потоков, и доступ к ресурсу могут получить сразу несколько из них. И это не просто случайное сходство :)
На самом деле мьютекс — это одноместный семафор. То есть, это семафор, счетчик которого изначально установлен в значении 1. Его еще называют «двоичным семафором», поскольку его счетчик может иметь только 2 значения — 1 («свободно») и 0 («занято»).
Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ