← Назад к вопросам

Что такое мьютекс?

2.2 Middle🔥 111 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мьютекс (Mutex)

Мьютекс (Mutual Exclusion) — это примитив синхронизации, который обеспечивает исключительный доступ к ресурсу. Только один поток может занимать мьютекс в один момент времени. Это предотвращает race conditions и обеспечивает потокобезопасность при работе с общими данными.

Концепция мьютекса

Мьютекс работает как замок:

Мьютекс свободен
     ↓
[Поток 1] берёт мьютекс → Мьютекс ЗАНЯТ
[Поток 2] ждёт → БЛОКИРОВАН
[Поток 3] ждёт → БЛОКИРОВАН
     ↓
[Поток 1] освобождает → Мьютекс СВОБОДЕН
     ↓
[Поток 2] берёт мьютекс → Мьютекс ЗАНЯТ
[Поток 1] ждёт → БЛОКИРОВАН

Мьютекс в Java: synchronized

В Java мьютекс реализован через ключевое слово synchronized:

public class MutexExample {
    private int counter = 0;
    
    // Способ 1: синхронизация метода
    public synchronized void increment() {
        counter++;  // Критическая секция
    }
    
    // Способ 2: синхронизация блока
    public void incrementBlock() {
        synchronized (this) {
            counter++;  // Критическая секция
        }
    }
    
    public synchronized int getCounter() {
        return counter;
    }
}

Синхронизация на методе

Когда вы используете synchronized на методе, блокируется весь объект:

public class SynchronizedMethod {
    private int value = 0;
    
    // Блокирует объект целиком
    public synchronized void setValue(int v) {
        this.value = v;
    }
    
    public synchronized int getValue() {
        return this.value;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SynchronizedMethod obj = new SynchronizedMethod();
        
        // Поток 1: вызывает setValue
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                obj.setValue(i);
            }
        });
        
        // Поток 2: вызывает getValue
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println(obj.getValue());
            }
        });
        
        // Только один может работать в раз — мьютекс!
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

Синхронизация блока (более гибко)

Можно синхронизировать только часть метода на разных объектах:

public class BlockSynchronization {
    private int count1 = 0;
    private int count2 = 0;
    private Object lock1 = new Object();  // Отдельный мьютекс
    private Object lock2 = new Object();  // Отдельный мьютекс
    
    public void incrementCount1() {
        // Синхронизация только count1
        synchronized (lock1) {
            count1++;
        }
        // Остальной код без синхронизации
        doExpensiveOperation();
    }
    
    public void incrementCount2() {
        // Синхронизация только count2
        synchronized (lock2) {
            count2++;
        }
        doExpensiveOperation();
    }
    
    private void doExpensiveOperation() {
        // Может выполняться параллельно
    }
    
    public static void main(String[] args) throws InterruptedException {
        BlockSynchronization obj = new BlockSynchronizedMethod();
        
        // Эти операции работают параллельно!
        // incrementCount1 не блокирует incrementCount2
        Thread t1 = new Thread(() -> obj.incrementCount1());
        Thread t2 = new Thread(() -> obj.incrementCount2());
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

Проблема: Race Condition без мьютекса

// ❌ ОПАСНО: без синхронизации
public class RaceCondition {
    private int counter = 0;
    
    public void increment() {
        counter++;  // НЕ атомарная операция!
        // Фактически это: read → increment → write
    }
    
    public static void main(String[] args) throws InterruptedException {
        RaceCondition obj = new RaceCondition();
        
        // 2 потока по 1000 инкрементов
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                obj.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                obj.increment();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        // Ожидаемый результат: 2000
        // Фактический результат: например, 1823 (потеряны обновления)
        System.out.println("Counter: " + obj.counter);
    }
}

// ✅ ПРАВИЛЬНО: с мьютексом
public class SafeCounter {
    private int counter = 0;
    
    public synchronized void increment() {
        counter++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SafeCounter obj = new SafeCounter();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                obj.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                obj.increment();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Counter: " + obj.counter);  // Всегда 2000
    }
}

Мьютекс для статических методов

public class StaticMutex {
    private static int globalCounter = 0;
    
    // Синхронизирует Class объект
    public static synchronized void incrementGlobal() {
        globalCounter++;
    }
    
    // Эквивалентно:
    public static void incrementGlobalBlock() {
        synchronized (StaticMutex.class) {
            globalCounter++;
        }
    }
}

Реентрантные мьютексы (Reentrant Locks)

Один поток может несколько раз захватить один мьютекс:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantMutexExample {
    private ReentrantLock lock = new ReentrantLock();
    
    public void method1() {
        lock.lock();
        try {
            System.out.println("In method1");
            method2();  // Тот же поток может снова захватить!
        } finally {
            lock.unlock();
        }
    }
    
    public void method2() {
        lock.lock();
        try {
            System.out.println("In method2");
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        ReentrantMutexExample obj = new ReentrantMutexExample();
        obj.method1();  // Работает без deadlock
    }
}

Deadlock с мьютексами

Мьютексы могут привести к deadlock-у:

// ❌ ОПАСНО: deadlock
public class DeadlockExample {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void method1() {
        synchronized (lock1) {
            System.out.println("Thread holding lock1, trying to get lock2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock2) {  // Может ждать lock2
                System.out.println("Got both locks in method1");
            }
        }
    }
    
    public void method2() {
        synchronized (lock2) {
            System.out.println("Thread holding lock2, trying to get lock1");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock1) {  // Может ждать lock1
                System.out.println("Got both locks in method2");
            }
        }
    }
    
    public static void main(String[] args) {
        DeadlockExample obj = new DeadlockExample();
        
        Thread t1 = new Thread(obj::method1);
        Thread t2 = new Thread(obj::method2);
        
        t1.start();
        t2.start();
        // DEADLOCK! Программа зависает
    }
}

// ✅ ПРАВИЛЬНО: всегда в одном порядке
public class NoDeadlock {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void method1() {
        synchronized (lock1) {
            synchronized (lock2) {
                System.out.println("Got both in method1");
            }
        }
    }
    
    public void method2() {
        // Всегда берём в одном порядке: lock1 → lock2
        synchronized (lock1) {
            synchronized (lock2) {
                System.out.println("Got both in method2");
            }
        }
    }
}

ReentrantLock vs synchronized

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

// ReentrantLock более гибкий
public class ReentrantLockAdvantages {
    private ReentrantLock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private int value = 0;
    
    // 1. Timeout при захвате
    public boolean tryAcquire() throws InterruptedException {
        return lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
    }
    
    // 2. Fairness (справедливость)
    private ReentrantLock fairLock = new ReentrantLock(true);
    
    // 3. Conditions для более гибкой синхронизации
    public void waitUntilNotEmpty() throws InterruptedException {
        lock.lock();
        try {
            while (value == 0) {
                notEmpty.await();  // Ждёт сигнала
            }
        } finally {
            lock.unlock();
        }
    }
    
    public void signalNotEmpty() {
        lock.lock();
        try {
            value = 1;
            notEmpty.signalAll();  // Будит ждущие потоки
        } finally {
            lock.unlock();
        }
    }
}

Лучшие практики

  1. Минимизируй критическую секцию
public synchronized void badWay() {
    synchronized (this) {
        // Много кода
        expensiveOperation();
        anotherExpensiveOperation();
    }
}

public void goodWay() {
    expensiveOperation();  // Без синхронизации
    anotherExpensiveOperation();  // Без синхронизации
    synchronized (this) {
        // Только обновление состояния
        updateState();
    }
}
  1. Используй finally или try-with-resources
lock.lock();
try {
    // код
} finally {
    lock.unlock();  // ОБЯЗАТЕЛЬНО!
}
  1. Избегай вложенных синхронизаций (риск deadlock)
  2. Документируй критические секции
  3. Используй утилиты (ConcurrentHashMap, AtomicInteger, и т.д.)

Заключение

Мьютекс — фундаментальный механизм для обеспечения потокобезопасности в многопоточных приложениях. В Java это реализовано через synchronized и ReentrantLock. Понимание мьютексов критично для:

  • Предотвращения race conditions
  • Гарантирования видимости изменений между потоками
  • Избежания deadlock-ов
  • Правильного проектирования потокобезопасного кода
Что такое мьютекс? | PrepBro