JavaRush/Java блог/Java Developer/В чем разница между мьютексом, монитором и семафором
Автор
Александр Мяделец
Руководитель команды разработчиков в CodeGym

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

Статья из группы 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 («занято»). Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
Комментарии (100)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Ислам
Уровень 33
22 февраля, 22:38
Весьма полезная лекция которая раскрывает эти понятия намного подробнее и интересней.
Abubakar Web Java Developer в Google
28 декабря 2023, 10:36
отличная лекция
Андрей Одинэсник
16 декабря 2023, 03:40
ого
obj.getMutex().isBusy() = true;
Миша Рич
Уровень 33
25 октября 2023, 09:03
Мьютекс (одноместный семафор) - флаг блокировки обьекта (только для одного потока) Монитор - скрытая логика мьютекса и семафора которая исполняется JVM Семафор - флаг блокировки обьекта (количество потоков, которые пользуются обьектом можно настроить).
ivan дворник
11 сентября 2023, 13:52
ребята купите какой-нибудь видеокурс (не алишев. слишком мало инфы). без него тяжело учиться пишет человек который отучился без видеокурса и уже закончил несколько видеокурсов
Denis Odesskiy
Уровень 32
26 мая 2023, 19:29
Популярной аналогией мьютекса в реальной жизни можно считать «пример с туалетом».
Так?
package com.javalerning.example.myownexample02;

import java.util.ArrayList;
import java.util.List;
/*
Пример с туалетом
 */
public class Main {
    private static final Sortire SORTIRE = new Sortire();

    public static void main(String[] args) {
        SufferingMan.createAndStartThread();
    }

    private static void defecate() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ignored) {
        }
    }

    private static void waitingsQueues() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ignored) {
        }
    }

//В методах класса Sortire, нужна синхронизация,
//чтобы Петя и Вася не оказались вместе в одном туалете:
    private static class Sortire {
        private Thread thread;

        public Thread getSortireStatus() {
            return thread;
        }

        private synchronized void setSortireStatus(Thread thread) {
            this.thread = thread;
        }

        private synchronized boolean trySetSortireIsFree(Thread thread) {
            if (this.thread == null) {
                this.thread = thread;
                return true;
            }
            return false;
        }
    }
Denis Odesskiy
Уровень 32
26 мая 2023, 19:29
private static class SufferingMan implements Runnable {
        private final String name;
        private final Thread thread;

        private SufferingMan(String name) {
            this.name = name;
            thread = new Thread(this, name);
        }

        @Override
        public void run() {
            boolean isAlreadyDefecated = false; //устанавливаем булевый флаг
            while (!isAlreadyDefecated) { //и в цикле проверяем
                if (SORTIRE.trySetSortireIsFree(thread)) { //если сортир занят человеком (потоком), то он:
                    System.out.println(name + " зашёл в туалет и справляет нужду");
                    defecate(); //справляет нужду в течении 2000 мс, и:
                    System.out.println(name + " вышел из туалета");
                    isAlreadyDefecated = true; //теперь устанавливаем флаг, что нужду справили
                    SORTIRE.setSortireStatus(null); //и освобождаем туалет
                } else if (!thread.equals(SORTIRE.getSortireStatus())) {
//иначе, если поток (наш человек) дёргает дверь туалета, а та закрыта, то:
                    System.out.println(name + " ожидает");
                    waitingsQueues(); //ожидает в очереди 2000 мс
                }
            }
        }

        //Фабрика очереди в туалет (потоков):
        private static void createAndStartThread() {
            List<String> listNames = new ArrayList<>() {
                {
                    add("Вася");
                    add("Петя");
                    add("Коля");
                }
            };
            for (String name : listNames) {
                SufferingMan man = new SufferingMan(name);
                man.thread.start();
            }
        }
    }
}
ivan дворник
10 апреля 2023, 15:54
В Java ключевое слово synchronized используется для обеспечения взаимного исключения и гарантирует, что только один поток может получить доступ к критической секции кода в данный момент. Ключевое слово synchronized может использоваться с блоком кода или методом, и оно использует монитор объекта для синхронизации. В Java монитор - это синхронизационный конструкт, который ассоциирован с объектом. Каждый объект в Java имеет встроенный монитор, который можно использовать для синхронизации доступа к методам и полям этого объекта. Когда поток входит в синхронизированный блок кода или метод, он получает монитор, связанный с объектом, и удерживает его до тех пор, пока не выйдет из блока кода или метода. Мьютекс - это синхронизационный примитив, который используется для защиты общих ресурсов от одновременного доступа нескольких потоков. В Java ключевое слово synchronized может использоваться для обеспечения взаимного исключения, что аналогично функциональности мьютекса. Семафор - это синхронизационный примитив, который используется для управления доступом к общему ресурсу. В Java класс Semaphore может использоваться для реализации семафоров, которые можно использовать для контроля доступа к общим ресурсам несколькими потоками. Таким образом, ключевое слово synchronized использует монитор объекта, который аналогичен мьютексу. Оно не использует семафор напрямую, но семафор можно реализовать с помощью класса Semaphore в Java.
Evgeny Balanda
Уровень 37
8 апреля 2023, 18:37
Странно, что в "Философия Java" Брюса Эккеля (4 издание) дают определения мьютекса и монитора наоборот: мьютекс - это механизм блокировки, а монитор - флаг, который используется для того, чтобы показать, что ресурс заблокирован: > Для решения проблемы соперничества потоков фактически все многопоточные схемы синхронизируют доступ к разделяемьм ресурсам. Это означает, что доступ к разделяе­мому ресурсу в один момент времени может получить только одна задача. Чаще всего это выполняется помещением фрагмента кода в предложение блокировки так, что одновременно пройти по этому фрагменту кода может только одна задача. Поскольку такое предложение блокировки дает эффект взаимного исключения (mutual exclusion), этот механизм часто называют мьютексом (mutex). > Каждый объект содержит объект простой блокировки (также называемый монитором), который автоматически является частью этого объекта (вам не придется писать для нее специального кода). Когда вы вызываете любой синхронизированный (synchronized) метод, объект переходит в состояние блокировки, и пока этот метод не закончит свою работу и не снимет блокировку, другие синхронизированные методы для объекта не могут быть вызваны.
Gans Electro
Уровень 50
8 мая 2023, 18:55
Видимо так и есть ssssssergeenkov Уровень 29 10 апреля, 21:54 В Java ключевое слово synchronized используется для обеспечения взаимного исключения и гарантирует, что только один поток может получить доступ к критической секции кода в данный момент. Ключевое слово synchronized может использоваться с блоком кода или методом, и оно использует монитор объекта для синхронизации. В Java монитор - это синхронизационный конструкт, который ассоциирован с объектом. Каждый объект в Java имеет встроенный монитор, который можно использовать для синхронизации доступа к методам и полям этого объекта. Когда поток входит в синхронизированный блок кода или метод, он получает монитор, связанный с объектом, и удерживает его до тех пор, пока не выйдет из блока кода или метода. Мьютекс - это синхронизационный примитив, который используется для защиты общих ресурсов от одновременного доступа нескольких потоков. В Java ключевое слово synchronized может использоваться для обеспечения взаимного исключения, что аналогично функциональности мьютекса. Семафор - это синхронизационный примитив, который используется для управления доступом к общему ресурсу. В Java класс Semaphore может использоваться для реализации семафоров, которые можно использовать для контроля доступа к общим ресурсам несколькими потоками. Таким образом, ключевое слово synchronized использует монитор объекта, который аналогичен мьютексу. Оно не использует семафор напрямую, но семафор можно реализовать с помощью класса Semaphore в Java.
ivan дворник
11 сентября 2023, 13:53
приятно что меня цитируют) спустя полгода могу сказать что я написал все правильно
Виктория
Уровень 111
24 февраля 2023, 11:27
Из комментариев поняла больше, чем из лекций. Вялікі дзякуй усім
Griboed
Уровень 30
21 февраля 2023, 08:02
Написано хорошо, понятно, но мне кажется, в конце немного понятия перепутаны. В статье написано, что мьютекс - это одноместный семафор, но судя по всему предыдущему объяснению, одноместным семафором является не мьютекс, а монитор. А мьютекс - это понятие, сходное не с семафором, а с разрешениями, которые семафор выдает. То есть, у монитора - один мьютекс, Соответственно, работает с данным кодом в один момент времени не более 1 потока. У семафора N разрешений. Соответственно, работают с данным кодом в один момент времени не более N потоков.
Lafaed
Уровень 36
27 февраля 2023, 20:32
Я тупой, или ява слишком умная, но мне показалось чуть чуть иначе : Монитор - это рука переключающая тумблеры (мониторит состояние) Мютекс - это панелька с одним тумблером (Запилинная прямо в экземпляр нашего чего-либо) Семафор - это панелька с "пачкой" тумблеров (Общий счётчик ) Ну типо скажем так : У пылесоса есть "мютекс" выключателей У телика есть "мютекс" выключателей У микроволновки есть "мютекс" выключателей А в квартире есть "семафор" розеток И конечно же "монитор" (рука) всё это шевелит
Griboed
Уровень 30
27 февраля 2023, 22:05
Судя по тому, как отличаются объяснения того, что такое Монитор в одних только этих лекциях на javarush, то могу предположить, что никто точно не знает, что же является в рамках java Монитором. Судя по объяснению в википедии, Монитор (синхронизация) - это "механизм взаимодействия и синхронизации процессов, обеспечивающий доступ к разделяемым ресурсам". То есть, это некий механизм. То, что нам известно о реализации данного механизма в java - что управление им обеспечивается за счет ключевого слова "synchronized". Ну а мьютекс - вроде как табличка на двери: "занято"/"свободно". Только в java эта табличка не на двери, а на каком-то объекте. Соответственно, "табличка" эта сама свое состояние не меняет, а этим занимается механизм, который для этого предназначен - а именно монитор.
Lafaed
Уровень 36
27 февраля 2023, 22:54
так я вроде бы как примерно так и предположил, что, "ручками" (монитор) обеспечивает синхронное подключение к некоему кол-ву "розеток" (семафор), ну и видимо включает "тумблеры" (мютекс) на неких "приборах бытовой техники". Пылесос включен (мютекс) и работает, занимая поток \ розетку (семафор), а монитор всё это контролирует, что втыкать в розетку а что нет.