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;
}
}
Тут будь‑які операції з балансом захищені м’ютексом, щоб не виникла гонка даних.
Приклад 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: Тривале утримання блокування. Чим довше потік тримає м’ютекс або семафор, тим довше інші чекають. Мінімізуйте час роботи всередині критичної секції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ