Professor Hans Noodles
41 уровень

В чем разница между мьютексом, монитором и семафором

Статья из группы Java Developer
Привет! Изучая многопоточность на JavaRush, ты часто встречал понятия «мьютекс» и «монитор». Сможешь сейчас, без подглядывания ответить, чем они отличаются? :) В чем разница между мьютексом, монитором и семафором - 1Если смог — молодец! Если же нет (а чаще всего так и бывает) — неудивительно. Понятия «мьютекс» и «монитор» действительно связаны между собой. Более того, читая лекции и смотря видео по многопоточности на внешних ресурсах в Интернете, ты столкнешься с еще одним похожим понятием — «семафор». Его функционал тоже во многом схож с монитором и мьютексом. Поэтому разберемся с этими тремя терминами, рассмотрим несколько примеров и окончательно упорядочим в голове понимание того, чем же они друг от друга отличаются :)

Мьютекс

Мьютекс — это специальный объект для синхронизации потоков. Он «прикреплен» к каждому объекту в Java — это ты уже знаешь :) Неважно, пользуешься ли ты стандартными классами или создал собственные классы, скажем, Cat и Dog: у всех объектов всех классов есть мьютекс. Название «мьютекс» происходит от английского «MUTual EXclusion» — «взаимное исключение», и это отлично отражает его предназначение. Как мы и говорили в одной из прошлых лекций, задача мьютекса — обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока. Популярной аналогией мьютекса в реальной жизни можно считать «пример с туалетом». Когда человек заходит в туалет, он закрывает изнутри дверь на замок. Туалет выполняет роль объекта, доступ к которому получают несколько потоков. Замок на двери туалета — роль мьютекса, а очередь из людей снаружи — роль потоков. Замок на двери — мьютекс туалета: он гарантирует, что внутри одновременно может находиться только один человек. В чем разница между мьютексом, монитором и семафором - 2Иными словами, только один поток в определенное время может работать с общими ресурсами. Попытки других потоков (людей) получить доступ к занятым ресурсам будут неудачными. У мьютекса есть несколько важных особенностей. Во-первых, возможны только два состояния — «свободен» и «занят». Это упрощает понимание принципа работы: можно провести параллели с булевыми переменными 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 в последнем примере, — это и есть монитор.

Семафор

Еще одно слово, с которым ты сталкиваешься при самостоятельном изучении многопоточности — «семафор». Давай разберемся что это такое, и чем он отличается от монитора и мьютекса. Семафор — это средство для синхронизации доступа к какому-то ресурсу. Его особенность заключается в том, что при создании механизма синхронизации он использует счетчик. Счетчик указывает нам, сколько потоков одновременно могут получать доступ к общему ресурсу. В чем разница между мьютексом, монитором и семафором - 3Семафоры в Java представлены классом Semaphore. При создании объектов-семафоров мы можем использовать такие конструкторы:

Semaphore(int permits)
Semaphore(int permits, boolean fair)
В конструктор мы передаем:
  • int permits — начальное и максимальное значение счетчика. То есть то, сколько потоков одновременно могут иметь доступ к общему ресурсу;

  • boolean fair — для установления порядка, в котором потоки будут получать доступ. Если fair = true, доступ предоставляется ожидающим потокам в том порядке, в котором они его запрашивали. Если же он равен false, порядок будет определять планировщик потоков.

Классический пример использования семафоров — задача об обедающих философах.
В чем разница между мьютексом, монитором и семафором - 4
Мы немного упростим ее условия, для лучшего понимания. Представь, что у нас есть 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). Что же у нас получится при запуске программы? Решена ли задача, не передерутся ли наши философы, ожидая своей очереди? :) Вот какой вывод в консоль мы получили: Сократ садится за стол Платон садится за стол Сократ поел! Он выходит из-за стола Платон поел! Он выходит из-за стола Аристотель садится за стол Пифагор садится за стол Аристотель поел! Он выходит из-за стола Пифагор поел! Он выходит из-за стола Фалес садится за стол Фалес поел! Он выходит из-за стола У нас все получилось! И хотя Фалесу пришлось обедать в одиночку, думаю, он на нас не в обиде :) Ты мог заметить некоторое сходство между мьютексом и семафором. У них, в общем-то, одинаковое предназначение: синхронизировать доступ к какому-то ресурсу. В чем разница между мьютексом, монитором и семафором - 5Разница только в том, что мьютекс объекта может захватить одновременно только один поток, а в случае с семафором используется счетчик потоков, и доступ к ресурсу могут получить сразу несколько из них. И это не просто случайное сходство :) На самом деле мьютекс — это одноместный семафор. То есть, это семафор, счетчик которого изначально установлен в значении 1. Его еще называют «двоичным семафором», поскольку его счетчик может иметь только 2 значения — 1 («свободно») и 0 («занято»). Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
Комментарии (80)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Магсумова Диана Уровень 45 Expert
27 ноября 2022
Это божественно!
Евгений Уровень 32
10 октября 2022
Монитор - невидимый для программиста кусок кода исполняемый jvm. Ну как бы и в чём прикол? Ну типа есть много всякого скрытого кода связанного с ключевыми словами или с другими языками программирования, это очевидно вроде. Но почему именно этот код имеет отдельное название и целое объяснение-пояснение. Есть умные люди в чате?) Объясните please зачем это знать 🙏
Maykro Уровень 16
30 сентября 2022
Прочитал несколько источников и понял так: Synchronized - ключевое слово, указывающее что для потоков, работающих с этим объектом/блоком кода, будет применяться механизм управления потоками (монитор).

synchronized (obj) {
   //какой-то код
}
Объект, указанный в ( ) содержит в себе маркер и может иметь 2 состояния: закрыт/открыт. Когда поток доходит до синхронизированного объекта/кода, он обращается к маркеру и запрашивает состояние. Это состояние контролирует монитор. Т.е. маркеры переключает монитор, а запрос делает поток. > Монитор - механизм контроля потоков при работе с сихронизированным объектом. Есть 2 типа механизма (монитора): мьютекс и семафор. 1. Механизм мьютекса ограничивает доступ к объекту/коду до 1 потока, т.е. одновременно с объектом может работать 1 поток. 2. Механизм семафора ограничивает доступ до N-го количества потоков одновременно, т.е. можно задать количество потоков. Мьютекс используется по умолчанию, а для задействования семафора нужно инициализировать класс Semaphor. У семафора через объект Semaphor можно настраивать: - количество потоков, которые могут одновременно работать с объектом;

Semaphore(int permits) // конструктор
- очередность работы потоков с объектом: последовательно (первый пришел - первый приступил к работе) или управление полностью отдается планировщику.

Semaphore(..., boolean fair) // конструктор
Hugon Уровень 34
22 июня 2022

        if (!full) {
               //Запрашиваем у семафора разрешение на выполнение
               sem.acquire();
               System.out.println (name + " садится за стол");

               // философ ест
               sleep(300);
               full = true;
Почему когда запрашиваем разрешение у семафора через метод acquire() у нас нет никакого условия, что если счетчик > 0, то вызываем System.out.println(name + " садится за стол"), а если счетчик 0, то Отказ?
Денис Уровень 16
20 мая 2022
Кто-нибудь может объяснить, пожалуйста, в чем разница: Пример первый, мы используем new semafor(2) и запускаем цикл, где создаются 10ть потоков с доступом к ограниченному ресурсу. Пример второй, мы используем Executors.newFixedThreadPool(2) и запускаем цикл, где создаются 10ть потоков ( submit() ). Разница в том, что: в первом примере создастся 10ть потоков и будет меньше производительность и ресурсозатратность, во втором примере будет задействовано всего 2 потока и они будут выполнять задачи, пока все не выполнятся ?
Alexey Pavlovsky Уровень 25
17 февраля 2022
Не говорите - нити. Это не профессионально.
Artamon Khakimov Уровень 35
24 января 2022
Мьютикс "приклеин" ко всем объектам Java, программист его не может увидеть, но благодаря Монитору мы можем влиять на Мьютикс с помощью слова synchronized. Семафор помечает кол-во нитей, которые могут иметь доступ к одному и тому же ресуру. Благодаря второму параметру при созданнии объекта Semaphore, мы можем регулировать, как нити будут получать ресурс. Если boolean fair = true -> последовательность такая, кто первый попросил общий ресур, тот первый и забирает, если false -> то за порядок потоков отвечает ПЛАНИРОВЩИК ПОТОКОВ.
SomeBoy Уровень 35
18 декабря 2021
Семафор - установливает количество нитей, которые могут воспользоваться объектом (Нужно создавать объект класса Semaphore) Мьютекс - дефолтно одноместный Семафор (Ничего создавать не нужно, встроен в класс Object, реализован в виде synchronized) Монитор - код, который выполняет JAVA машина, при использовании Мьютекса - не виден программисту. Я всё понял. Поправьте, если ошибся.
Игорь Уровень 33
20 ноября 2021
Во первых ничего пока не понял из статьи.. Ну думаю еще почитаю пару раз и придёт осмысление. Прочитал сложилось впечатление что все почти что синонимы но не совсем. Во вторых, решал похожу задачку с философами через ThreadPool, логика была такая, создавал пул на 2 потока, загружал в пул 5 задач, и запускал shutdown(). После этого результат был такой же как здесь с Semporhe(). Может кому то будет полезно.
5 мая 2021
Добавлю от себя, что мьютекс и семафор - объекты ядра операционной системы, а взаимодействия с ними происходит посредством API ОС (WinAPI, Linux API). Высокоуровневые языки просто дёргают эти API функции по большей части, так что полноценная многопоточность реализуема прямо из-под C/asm. Это я к тому, чтобы было понимание, что механизм этот во всех языках работает примерно по-одинаковому, и что магия работы этого вшита не в сам язык, а в ОС. Практической ценности, конечно, это замечание не имеет, однако, как интересняшка - вполне себе.