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 («занято»). Вот и все! Как видишь, все оказалось не таким уж и запутанным :) Теперь, если ты захочешь изучить тему многопоточности подробнее в Интернете, тебе будет чуть проще ориентироваться в понятиях. До встречи на следующих уроках!
Комментарии (104)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Light_Day :) Уровень 36
14 июля 2024
👍
Private Joker Уровень 36
14 июня 2024
GPT дает другие сведения, особенно про Мьютекс.
Максим Li Уровень 40
22 мая 2024
Хорошая статья!
Olexandr Уровень 38
1 апреля 2024
Отличная лекция, все по полочкам!
Ислам Уровень 33
22 февраля 2024
Весьма полезная лекция которая раскрывает эти понятия намного подробнее и интересней.
Abubakar Уровень 30
28 декабря 2023
отличная лекция
Андрей Уровень 51
16 декабря 2023
ого

obj.getMutex().isBusy() = true;
Миша Рич Уровень 33
25 октября 2023
Мьютекс (одноместный семафор) - флаг блокировки обьекта (только для одного потока) Монитор - скрытая логика мьютекса и семафора которая исполняется JVM Семафор - флаг блокировки обьекта (количество потоков, которые пользуются обьектом можно настроить).
ivan Уровень 40
11 сентября 2023
ребята купите какой-нибудь видеокурс (не алишев. слишком мало инфы). без него тяжело учиться пишет человек который отучился без видеокурса и уже закончил несколько видеокурсов
Denis Odesskiy Уровень 37
26 мая 2023

Популярной аналогией мьютекса в реальной жизни можно считать «пример с туалетом».
Так?

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;
        }
    }