User Professor Hans Noodles
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 («занято»). Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
Комментарии (59)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ СДЕЛАТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
5 мая 2021
Добавлю от себя, что мьютекс и семафор - объекты ядра операционной системы, а взаимодействия с ними происходит посредством API ОС (WinAPI, Linux API). Высокоуровневые языки просто дёргают эти API функции по большей части, так что полноценная многопоточность реализуема прямо из-под C/asm. Это я к тому, чтобы было понимание, что механизм этот во всех языках работает примерно по-одинаковому, и что магия работы этого вшита не в сам язык, а в ОС. Практической ценности, конечно, это замечание не имеет, однако, как интересняшка - вполне себе.
Anonymous #2491313 Уровень 35
13 апреля 2021
А есть ещё класс Lock...
turkish joe Уровень 0
13 февраля 2021
На самом деле мьютекс — это одноместный семафор. Но https://ru.wikipedia.org/wiki/%D0%9C%D1%8C%D1%8E%D1%82%D0%B5%D0%BA%D1%81 >>Мью́текс (англ. mutex, от mutual exclusion — «взаимное исключение») — примитив синхронизации, обеспечивающий взаимное исключение исполнения критических участков кода[1]. Классический мьютекс отличается от двоичного семафора наличием эксклюзивного владельца, который и должен его освобождать (т.е. переводить в незаблокированное состояние)[2]. Разве корректно так называть мьютекс? Вот вроде пояснение(сам не до конца разобрался) https://coderoad.ru/62814/%D0%A0%D0%B0%D0%B7%D0%BD%D0%B8%D1%86%D0%B0-%D0%BC%D0%B5%D0%B6%D0%B4%D1%83-%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%BC-%D1%81%D0%B5%D0%BC%D0%B0%D1%84%D0%BE%D1%80%D0%BE%D0%BC-%D0%B8-mutex
🦔 Виктор Уровень 20, Москва, Россия Expert
23 января 2021
Спасибо, великолепная статья, которая довольно нетривиальные темы расставляет на свои места. Очень доступно и понятно, с наглядными примерами. Всё бы так! «Мьютекс - флаг блокировки объекта (только для одного потока). Монитор - скрытая логика мьютекса, которая исполняется JVM Семафор - это отдельный класс, объект которого нужно создавать, чтобы им пользоваться для блокировки объекта (количество потоков, которые пользуются объектом можно настроить)» © Михаил Турчанов. — У нас все получилось!
Ivan Chuvikov Уровень 24, Санкт-Петербург, Россия
24 декабря 2020
Очень доступно и понятно вроде как даже пока)
Alexander Kolesnik Уровень 35, Москва, Россия
14 декабря 2020
1. Семафор - примитив синхронизации работы процессов и потоков, выраженный счетчиком количества потоков, у которых единовременный доступ к объекту. 2. Мьютекс - одноместный семафор(доступ только у одного потока) 3. Монитор - скрытая логика, реализация java-машиной блокировки-разблокировки доступа к объекту. Выражена ключевым словом synchrinized. Избавляет от необходимости ручного использования примитивов синхронизации.
wan-derer.ru Уровень 40, Москва, Россия
17 октября 2020
Проблема в понимании терминов исходит из того что они (термины) вводятся от балды. Та же "синхронизация". Синхронизация - это приведение в соответствие чего-то с чем-то. Значит нужно что-то одно и что-то другое, и они должны стать соответствующими друг другу - синхронными. Пример из электроники: есть два сигнала одинаковой формы, но с разной фазой. Подкручивая фазу одного из них мы добиваемся их полной одинаковости. В данном случае (не всегда!) синхронность == синфазности. Здесь же (в программировании) всё ровно наоборот. "Сигналы" нив коем случае не должны "совпасть", наоборот, их надо разнести во времени. Здесь подошёл бы термин "мультиплексирование". Но лучше сказать - разделение. Так и живём. Кто-то безграмотный влепил красивое слово, не понимая что оно означает. А все мы вместо понимания должны запомнить что синхронизация == разделение. И так во всём: начиная от "классов" и "методов", далее везде.
Александр Уровень 35, Минск
5 августа 2020
Не много не понял про мютекс объекта. Допусти я сделал synchrinized(this) блок, то вроде понятно. Объект который вызвал метод блокирует мютекс при попадание в блок, как только он закончил работу он его освобождает. А какой смысл делать отдельный объект sunchronizer(obj)? я вызвал метод в котором есть такой блок другим объектом, obj его заблокировал и как он попадет в этот блок?...
Herr Ives Уровень 30
2 мая 2020
получается у каждого обьекта два флага унаследованных от класса Object? или это один и тот же флаг?
Pavlo Buidenkov Уровень 41
28 марта 2020
Кому интересна история. Проблема про 5 философов была сформулирована Дейкстрой (Edsger W. Dijkstra) в 1965. Dining philosophers problem Мы можем решить эту проблему используя 5 одноместных семафор-"вилок" и каждому философу дать по-паре ссылок на них (левая, правая) Semaphore fork1 = new Semaphore(1); Semaphore fork2 = new Semaphore(1); Philosopher philosopher1 = new Philosopher("Socrates", fork1, fork2); ... также нужно не забыть что теперь нам нужно .acquire() и .release() два семафора-"вилки" внутри метода run() нашего философа.