1. Мьютекс (Mutex): что это такое и как работает
Мьютекс (от английского «mutual exclusion» — «взаимное исключение») — механизм, который позволяет только одному потоку одновременно выполнять критическую секцию кода. Если мьютекс занят (захвачен другим потоком), остальные потоки ждут, пока он освободится.
В Java роль мьютекса часто выполняет объект, на котором синхронизируется код: synchronized. Начиная с 5-й версии Java, появился класс ReentrantLock — более явная и гибкая реализация мьютекса.
Схематично
Представьте комнату с единственным ключом (мьютексом). Чтобы войти, нужно взять ключ. Если ключа нет (его уже кто-то взял), вы ждёте у двери. Как только ключ возвращается на место (мьютекс освобождается), следующий человек может войти.
Синтаксис мьютекса в Java
Через synchronized (классика):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Здесь весь метод increment защищён мьютексом — только один поток может выполнять его в данный момент.
Через ReentrantLock (более гибко):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // Захватываем мьютекс
try {
count++;
} finally {
lock.unlock(); // Обязательно освобождаем!
}
}
}
Важно! Всегда освобождайте мьютекс в блоке finally, иначе можете получить «вечную блокировку» (deadlock) — и программа зависнет.
Когда нужен мьютекс?
Мьютекс необходим, когда к ресурсу должен подходить только один поток за раз. Это может быть переменная, файл или база данных. Особенно важно использовать мьютекс, если работа с ресурсом не атомарна: даже простое count++ на самом деле состоит из трёх шагов — прочитать значение, увеличить и записать обратно. Без мьютекса несколько потоков могут вмешаться между шагами и вызвать гонку данных.
2. Семафор (Semaphore): зачем он нужен и как работает
Семафор — «регулятор», который разрешает одновременно работать с ресурсом нескольким потокам, но не более заданного количества. Если лимит исчерпан, остальные потоки ждут своей очереди.
Аналогия: парковка на 3 машины. Если все места заняты, вновь прибывшие ждут, пока кто-то не уедет.
Синтаксис семафора в Java
Для этого используется класс Semaphore из пакета java.util.concurrent:
import java.util.concurrent.Semaphore;
public class ParkingLot {
private final Semaphore spots;
public ParkingLot(int places) {
this.spots = new Semaphore(places);
}
public void parkCar(String car) throws InterruptedException {
spots.acquire(); // Пытаемся занять место (если нет — ждём)
try {
System.out.println(car + " припарковалась.");
Thread.sleep(1000); // Машина стоит на парковке
} finally {
spots.release(); // Освобождаем место
System.out.println(car + " уехала.");
}
}
}
Использование:
ParkingLot parking = new ParkingLot(3);
for (int i = 1; i <= 5; i++) {
final String car = "Машина " + i;
new Thread(() -> {
try {
parking.parkCar(car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Результат: одновременно на парковке не будет больше трёх машин — остальные ждут.
Как работает семафор?
- При создании семафора задаётся количество «разрешений» (permits).
- Метод acquire() пытается взять разрешение: если есть свободное — поток проходит, если нет — ждёт.
- Метод release() возвращает разрешение обратно.
- Семафор с одним разрешением ведёт себя почти как мьютекс, но без «владельца».
3. Мьютекс и семафор: в чём разница?
| Характеристика | Мьютекс (Mutex) | Семафор (Semaphore) |
|---|---|---|
| Количество потоков | Только один | Несколько (ограниченное число) |
| Применение | Защита ресурса | Ограничение доступа (например, пул) |
| API в Java | |
|
| Управление | Обычно «владелец» | Может освобождать любой поток |
| Типичный сценарий | Общий счётчик, объект | Пул соединений, парковка, лимит |
- Мьютекс — для случаев, когда нужен эксклюзивный доступ.
- Семафор — когда можно пускать несколько, но не всех.
Аналогия: мьютекс — уборная с одной кабинкой; семафор — уборная с тремя кабинками.
4. Практические примеры задач
Пример 1: Мьютекс для защиты критической секции
Допустим, у нас есть общий банк, и несколько потоков переводят деньги между счетами. Операции должны быть атомарными.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccount(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
public void withdraw(int amount) {
lock.lock();
try {
if (balance >= amount) {
balance -= amount;
}
} finally {
lock.unlock();
}
}
public int getBalance() {
return balance;
}
}
Здесь любые операции с балансом защищены мьютексом, чтобы не возникло race condition.
Пример 2: Семафор для ограничения доступа
Сервер может одновременно обрабатывать только 2 клиента (например, из‑за лицензии).
import java.util.concurrent.Semaphore;
public class Server {
private final Semaphore connections = new Semaphore(2);
public void handleRequest(String client) throws InterruptedException {
connections.acquire();
try {
System.out.println(client + " подключился к серверу.");
Thread.sleep(2000); // Имитация обработки запроса
} finally {
connections.release();
System.out.println(client + " отключился.");
}
}
}
Использование:
Server server = new Server();
for (int i = 1; i <= 5; i++) {
final String client = "Клиент " + i;
new Thread(() -> {
try {
server.handleRequest(client);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Результат: одновременно сервер обслуживает не более двух клиентов.
5. Особенности и нюансы использования
Мьютекс: всегда освобождай!
Очень важно не забывать вызывать unlock() (или выходить из синхронизированного блока) даже при исключениях. Используйте try-finally:
lock.lock();
try {
// критическая секция
} finally {
lock.unlock();
}
Если забыть — можно получить «вечную блокировку»; другие потоки будут ждать бесконечно.
Семафор: можно ли освободить чужой поток?
В отличие от мьютекса, release() может вызвать любой поток, даже тот, который не делал acquire(). Это иногда удобно, но легко ошибиться — соблюдайте дисциплину.
Semaphore с одним разрешением = мьютекс?
Почти. Но у семафора нет понятия «владельца»: любое освобождение увеличивает счётчик разрешений. У мьютекса же освобождать должен тот, кто захватил.
Не путайте семафор и пул
Семафор — это не пул объектов, а только «счётчик разрешений». Его часто используют для реализации пулов (например, пул соединений к БД), но сам по себе он ничего не хранит.
6. Типичные ошибки при работе с мьютексами и семафорами
Ошибка №1: Забыли вызвать unlock/release. Если вы захватили мьютекс или семафор, но не вызвали unlock() или release(), другие потоки могут зависнуть навсегда. Всегда используйте try-finally, чтобы гарантировать освобождение блокировки даже при исключениях.
Ошибка №2: Синхронизация на неправильном объекте. Если синхронизироваться на переменной, которая не общая для всех потоков (например, на локальной переменной или строковом литерале), синхронизация работать не будет.
Ошибка №3: Двойное освобождение. В случае с семафором: если вызвать release() больше раз, чем было acquire(), количество разрешений увеличится сверх лимита. Следите за балансом!
Ошибка №4: Использование семафора вместо мьютекса (или наоборот). Если нужен эксклюзивный доступ, используйте мьютекс (synchronized или Lock). Если нужно ограничить число одновременно работающих потоков — используйте Semaphore.
Ошибка №5: Долгое удержание блокировки. Чем дольше поток держит мьютекс или семафор, тем дольше другие ждут. Минимизируйте время работы внутри критической секции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ