Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужен Mutex
Mutex (mutual exclusion) — это один из самых фундаментальных концептов многопоточного программирования. За 10+ лет я видел системы, которые тормозили или крашились из-за race conditions. Давайте разберёмся почему Mutex критичный.
Проблема: Race Condition
Представьте счётный банков с балансом 1000 рублей. Два потока (два клиента) одновременно снимают по 500 рублей:
public class BankAccount {
private int balance = 1000;
// БЕЗ Mutex (race condition!)
public void withdraw(int amount) {
int temp = balance; // Шаг 1
temp = temp - amount; // Шаг 2
balance = temp; // Шаг 3
}
}
// Timeline:
Thread 1: balance = 1000, читает: temp1 = 1000
Thread 2: balance = 1000, читает: temp2 = 1000
Thread 1: вычисляет: temp1 = 1000 - 500 = 500
Thread 2: вычисляет: temp2 = 1000 - 500 = 500
Thread 1: пишет: balance = 500
Thread 2: пишет: balance = 500 ← НЕПРАВИЛЬНО! Должно быть 0
Результат: оба снимали по 500, но баланс = 500 (вместо 0)
Банк потерял 500 рублей!
Это race condition. Результат зависит от timing, что делает bug непредсказуемым.
Решение: Mutex
Mutex гарантирует, что только один поток может выполнять код одновременно:
public class BankAccount {
private int balance = 1000;
private final Object lock = new Object(); // Mutex
// С Mutex (безопасно!)
public void withdraw(int amount) {
synchronized (lock) { // Захватываем mutex
int temp = balance; // Шаг 1
temp = temp - amount; // Шаг 2
balance = temp; // Шаг 3
} // Освобождаем mutex
}
}
// Timeline:
Thread 1: захватывает lock
Thread 1: balance = 1000, читает: temp1 = 1000
Thread 1: вычисляет: temp1 = 1000 - 500 = 500
Thread 1: пишет: balance = 500
Thread 1: освобождает lock
Thread 2: захватывает lock (ждал, пока Thread 1 кончит)
Thread 2: balance = 500, читает: temp2 = 500
Thread 2: вычисляет: temp2 = 500 - 500 = 0
Thread 2: пишет: balance = 0
Thread 2: освобождает lock
Результат: balance = 0 ✅ Правильно!
Как работает Mutex в Java
1. Synchronized блок
private final Object lock = new Object();
public void criticalSection() {
synchronized (lock) {
// Только один поток может быть здесь одновременно
// Остальные потоки ждут
// ...
} // Автоматически освобождаем lock
}
2. Synchronized метод
public class SafeCounter {
private int count = 0;
// Эквивалентно: synchronized(this)
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// Для static методов: synchronized(ClassName.class)
public static synchronized void staticMethod() {
// Lock на класс, не на instance
}
3. ReentrantLock (современный подход)
public class BankAccount {
private int balance = 1000;
private final Lock lock = new ReentrantLock();
public void withdraw(int amount) {
lock.lock(); // Захватываем lock
try {
int temp = balance;
temp = temp - amount;
balance = temp;
} finally {
lock.unlock(); // Всегда освобождаем, даже если exception
}
}
}
Реальные примеры из моего опыта
Пример 1: Payment processing
public class PaymentProcessor {
private final Lock transactionLock = new ReentrantLock();
private Account fromAccount;
private Account toAccount;
public void transfer(BigDecimal amount) throws InsufficientFundsException {
transactionLock.lock();
try {
// Без lock: race condition!
// A может видеть balance от B пока B пишет
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
fromAccount.debit(amount);
toAccount.credit(amount);
// Database запрос
transactionRepository.save(transaction);
} finally {
transactionLock.unlock();
}
}
}
Без Mutex:
- Thread A checks: balance = 1000 > 500? Yes
- Thread B checks: balance = 1000 > 500? Yes
- Thread A debits 500: balance = 500
- Thread B debits 500: balance = -500 ← NEGATIVE!
С Mutex: Только один поток в раз может проверить и debited.
Пример 2: Inventory management
public class Inventory {
private Map<String, Integer> stock = new HashMap<>();
private final Lock inventoryLock = new ReentrantLock();
public boolean reserveItem(String itemId) {
inventoryLock.lock();
try {
Integer available = stock.getOrDefault(itemId, 0);
if (available > 0) {
stock.put(itemId, available - 1);
return true; // Reserved
}
return false; // Out of stock
} finally {
inventoryLock.unlock();
}
}
}
Без Mutex:
- Customer A checks: item available? Yes (stock = 5)
- Customer B checks: item available? Yes (stock = 5)
- Customer A reserves: stock = 4
- Customer B reserves: stock = 4
- But both got reserved from stock of 5! One oversold!
Tipos Mutex'a в Java
1. Synchronized (встроенный)
public synchronized void method() { } // На instance
public static synchronized void method() { } // На класс
synchronized(lock) { } // На объект
Плюсы: Встроенный, автоматический unlock Минусы: Нельзя interruptible, нельзя timeout, простой
2. ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
Плюсы: Гибкий, interruptible, can set timeout, fairness Минусы: Нужно помнить unlock, более многословный
3. ReadWriteLock (когда много читают, мало пишут)
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public int read() {
rwLock.readLock().lock(); // Много читателей могут быть одновременно
try {
return value;
} finally {
rwLock.readLock().unlock();
}
}
public void write(int v) {
rwLock.writeLock().lock(); // Только один писатель
try {
value = v;
} finally {
rwLock.writeLock().unlock();
}
}
4. Semaphore (когда нужно N потоков максимум)
Semaphore semaphore = new Semaphore(3); // Max 3 потока
public void limitedResource() throws InterruptedException {
semaphore.acquire(); // Захватываем permit
try {
// Max 3 потока здесь одновременно
} finally {
semaphore.release();
}
}
Deadlock (опасность Mutex'а)
// ❌ DEADLOCK!
public class Account {
private int balance;
private final Lock lock = new ReentrantLock();
public void transfer(Account other, int amount) {
lock.lock();
try {
other.lock.lock(); // ← DEADLOCK если other тоже пытается transfer в нас
try {
this.balance -= amount;
other.balance += amount;
} finally {
other.lock.unlock();
}
} finally {
lock.unlock();
}
}
}
// Сценарий deadlock:
Thread 1: transfer(account1 → account2, 100):
1. Захватывает lock на account1 ✓
2. Пытается захватить lock на account2 ← ЖДЁТ
Thread 2: transfer(account2 → account1, 200):
1. Захватывает lock на account2 ✓
2. Пытается захватить lock на account1 ← ЖДЁТ
// Они друг друга ждут! DEADLOCK!
Решение: Ordered locking
public void transfer(Account other, int amount) {
// Всегда захватываем в одинаковом порядке (по ID)
Account first = this.id < other.id ? this : other;
Account second = this.id < other.id ? other : this;
first.lock.lock();
try {
second.lock.lock();
try {
// Transfer logic
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
Performance consideration
Mutex имеет overhead:
// Synchronized быстрее для простых операций
for (int i = 0; i < 1000000; i++) {
counter++; // 1-10ns per operation
}
// Lock/unlock может быть 50-100ns (в зависимости от contention)
synchronized(lock) {
counter++;
}
Оптимизация:
- Minimize critical section (короче lock'а = быстрее)
- Reduce contention (меньше потоков = меньше конфликтов)
- Use ReadWriteLock если много читают
- Use StampedLock (Java 8+) для очень высокой performance
Best practices
- Всегда используйте try-finally (или try-with-resources)
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // Даже если exception!
}
- Минимизируйте время в critical section
// ❌ Плохо: I/O inside lock
synchronized(lock) {
httpClient.get(url); // SLOW!
}
// ✅ Хорошо: только самое necessary
synchronized(lock) {
if (needsRefresh) {
shouldFetch = true;
}
}
if (shouldFetch) {
data = httpClient.get(url); // Outside lock
}
- Не вызывайте unknown методы в critical section
// ❌ Опасно: что если callback захватит другой lock?
synchronized(lock) {
listener.onUpdate(); // Unknown code!
}
Вывод
Mutex нужен для:
- Защиты shared state
- Prevent race conditions
- Гарантирования consistent data
- Correct результатов в многопоточных приложениях
Без Mutex в многопоточной системе: хаос, data corruption, недетерминированное поведение.
С Mutex: безопасность, но нужно быть careful про deadlocks и performance.