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 («занято»). Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
Комментарии (70)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Денис Уровень 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, Москва, Russian Federation
18 декабря 2021
Семафор - установливает количество нитей, которые могут воспользоваться объектом (Нужно создавать объект класса Semaphore) Мьютекс - дефолтно одноместный Семафор (Ничего создавать не нужно, встроен в класс Object, реализован в виде synchronized) Монитор - код, который выполняет JAVA машина, при использовании Мьютекса - не виден программисту. Я всё понял. Поправьте, если ошибся.
Игорь Уровень 33, Москва , Россия
20 ноября 2021
Во первых ничего пока не понял из статьи.. Ну думаю еще почитаю пару раз и придёт осмысление. Прочитал сложилось впечатление что все почти что синонимы но не совсем. Во вторых, решал похожу задачку с философами через ThreadPool, логика была такая, создавал пул на 2 потока, загружал в пул 5 задач, и запускал shutdown(). После этого результат был такой же как здесь с Semporhe(). Может кому то будет полезно.
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
Очень доступно и понятно вроде как даже пока)