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

Что является монитором при работе с wait

1.8 Middle🔥 201 комментариев
#Многопоточность#Основы Java

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

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

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

# Монитор при работе с wait() и notify()

Монитор — это внутренний механизм синхронизации, связанный с каждым объектом в Java. Это то, что позволяет потокам координировать работу через wait() и notify().

Что такое монитор

Каждый объект в Java имеет:

1. Блокировка (lock)
2. Очередь ожидания (wait queue) для потоков, вызвавших wait()

Это вместе называется монитор.

public class Object {  // Все объекты в Java наследуют от Object
    // В каждом объекте есть встроенный монитор
    public synchronized void someMethod() { }  // synchronized = работает с монитором
    public void wait() { }                      // wait() = ждёт сигнала от монитора
    public void notify() { }                    // notify() = сигнал монитору
}

Как работает монитор

1. Поток пытается войти в synchronized блок
   ↓
2. Если монитор свободен → поток входит
   ↓
3. Если монитор занят → поток ждёт в очереди (BLOCKED)
   ↓
4. Когда текущий поток выходит → следующий из очереди входит

Пример: wait() и notify()

public class ProducerConsumer {
    private int value = 0;
    private boolean ready = false;
    
    // Производитель
    public synchronized void produce(int val) throws InterruptedException {
        while (ready) {
            wait();  // Ждёшь, пока потребитель заберёт значение
        }
        this.value = val;
        this.ready = true;
        notify();  // Пробуждаешь потребителя
    }
    
    // Потребитель
    public synchronized int consume() throws InterruptedException {
        while (!ready) {
            wait();  // Ждёшь, пока производитель произведёт
        }
        int val = this.value;
        this.ready = false;
        notify();  // Пробуждаешь производителя
        return val;
    }
}

Что здесь монитор? — это объект ProducerConsumer сам!

Когда ты пишешь synchronized, wait(), notify() — ты работаешь с монитором этого объекта.

Явное указание монитора

Монитор не привязан к методу, а к объекту. Можно явно указать, чей монитор использовать:

public class SharedBuffer {
    private int[] buffer = new int[10];
    private int size = 0;
    private Object lock = new Object();  // Явный монитор
    
    public void add(int value) {
        synchronized (lock) {  // Используем lock как монитор
            if (size < buffer.length) {
                buffer[size++] = value;
                lock.notifyAll();  // Пробуждаем потоки, ждущие на этом мониторе
            }
        }
    }
    
    public int remove() throws InterruptedException {
        synchronized (lock) {  // Тот же монитор
            while (size == 0) {
                lock.wait();  // Ждём сигнала от этого монитора
            }
            return buffer[--size];
        }
    }
}

synchronized на методе vs synchronized на объекте

public class Counter {
    private int count = 0;
    private Object lockObj = new Object();
    
    // ✅ Способ 1: synchronized на методе
    // Монитор = сам объект Counter
    public synchronized void increment1() {
        count++;
    }
    
    // ✅ Способ 2: synchronized на объекте
    // Монитор = этот объект (this)
    public void increment2() {
        synchronized (this) {
            count++;
        }
    }
    
    // ✅ Способ 3: synchronized на отдельном объекте
    // Монитор = lockObj
    public void increment3() {
        synchronized (lockObj) {
            count++;
        }
    }
}

// Способы 1 и 2 эквивалентны
// Способ 3 изолирует логику (lockObj — отдельный монитор)

Очередь ожидания (wait queue)

Каждый монитор имеет очередь потоков, которые вызвали wait():

public class MonitorExample {
    private Object monitor = new Object();
    
    public static void main(String[] args) {
        MonitorExample obj = new MonitorExample();
        
        // Поток 1
        new Thread(() -> {
            synchronized (obj.monitor) {
                System.out.println("Thread 1: waiting...");
                try {
                    obj.monitor.wait();  // Входит в wait queue
                    System.out.println("Thread 1: woke up!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // Поток 2
        new Thread(() -> {
            synchronized (obj.monitor) {
                System.out.println("Thread 2: waiting...");
                try {
                    obj.monitor.wait();  // Входит в wait queue
                    System.out.println("Thread 2: woke up!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // Поток 3 (уведомитель)
        try {
            Thread.sleep(100);
            synchronized (obj.monitor) {
                System.out.println("Thread 3: notifying all...");
                obj.monitor.notifyAll();  // Пробуждает ВСЕ потоки из wait queue
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// Output:
// Thread 1: waiting...
// Thread 2: waiting...
// Thread 3: notifying all...
// Thread 1: woke up!
// Thread 2: woke up!

wait() vs notify() vs notifyAll()

public class NotifyExample {
    private Object monitor = new Object();
    
    // ❌ МОЖЕТ БЫТЬ ПРОБЛЕМА: notify() пробуждает только ОДИН поток
    public void badNotify() {
        synchronized (monitor) {
            monitor.notify();  // Пробуждает только одного случайного потока
            // Другие остаются в wait queue
        }
    }
    
    // ✅ ЛУЧШЕ: notifyAll() пробуждает ВСЕ
    public void goodNotify() {
        synchronized (monitor) {
            monitor.notifyAll();  // Пробуждает всех
            // Но только один войдёт, остальные снова будут ждать
        }
    }
}

wait() ВСЕГДА должен быть в while, не if

public class Producer {
    private boolean ready = false;
    private Object monitor = new Object();
    
    // ❌ НЕПРАВИЛЬНО: используешь if
    public void consumeWrong() throws InterruptedException {
        synchronized (monitor) {
            if (!ready) {  // ← ОПАСНО!
                monitor.wait();  // Просыпаешься один раз
            }
            // А условие может стать false снова!
        }
    }
    
    // ✅ ПРАВИЛЬНО: используешь while
    public void consumeRight() throws InterruptedException {
        synchronized (monitor) {
            while (!ready) {  // ← БЕЗОПАСНО!
                monitor.wait();  // Проверяешь условие снова после пробуждения
            }
            // Теперь уверен, что ready = true
        }
    }
}

Пример с классом условия (Condition из java.util.concurrent)

Вместо wait/notify можно использовать более современный подход:

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

public class ModernProducerConsumer {
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();  // Условие для потребителя
    private Condition notFull = lock.newCondition();   // Условие для производителя
    
    private int[] buffer = new int[10];
    private int size = 0;
    
    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (size == buffer.length) {
                notFull.await();  // Ждём, пока не станет место
            }
            buffer[size++] = value;
            notEmpty.signalAll();  // Пробуждаем потребителя
        } finally {
            lock.unlock();
        }
    }
    
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (size == 0) {
                notEmpty.await();  // Ждём, пока будет что-то
            }
            int value = buffer[--size];
            notFull.signalAll();  // Пробуждаем производителя
            return value;
        } finally {
            lock.unlock();
        }
    }
}

Мониторы и synchronized блоки

public class Account {
    private int balance = 0;
    
    // Монитор = Account.class (статический монитор)
    public static synchronized void staticOperation() { }
    
    // Монитор = этот объект (this)
    public synchronized void deposit(int amount) {
        balance += amount;
    }
    
    // Можешь использовать разные мониторы для разных частей
    private Object lockRead = new Object();
    private Object lockWrite = new Object();
    
    public int read() {
        synchronized (lockRead) {
            return balance;
        }
    }
    
    public void write(int amount) {
        synchronized (lockWrite) {
            balance = amount;
        }
    }
    // Теперь read и write не блокируют друг друга
}

Правила работы с мониторами

1. wait() может быть вызван ТОЛЬКО внутри synchronized блока
2. notify() может быть вызван ТОЛЬКО внутри synchronized блока
3. Монитор = объект, на котором вызвал wait/notify
4. Когда вызовешь wait(), поток ОТПУСКАЕТ монитор
5. После пробуждения (notify) поток заново захватывает монитор
6. Используй notifyAll() вместо notify() для безопасности
7. ВСЕГДА используй while, не if, для проверки условия перед wait()

Что произойдёт, если неправильно использовать

// ❌ ОШИБКА: вызвал wait() без synchronized
public void badWait() throws InterruptedException {
    monitor.wait();  // IllegalMonitorStateException!
}

// ❌ ОШИБКА: ждёшь на одном объекте, а notify на другом
synchronized (obj1) {
    obj1.wait();  // Ждёшь на obj1
}
synchronized (obj2) {
    obj2.notify();  // Уведомляешь obj2 — не поможет!
}

// ✅ ПРАВИЛЬНО: одинаковый объект
synchronized (monitor) {
    monitor.wait();
}
synchronized (monitor) {
    monitor.notify();
}

Практический пример: простой синхронизированный стек

public class SynchronizedStack<T> {
    private java.util.Stack<T> stack = new java.util.Stack<>();
    private Object monitor = new Object();
    
    public void push(T item) throws InterruptedException {
        synchronized (monitor) {
            stack.push(item);
            monitor.notifyAll();  // Пробуждаем потоки, ждущие в pop()
        }
    }
    
    public T pop() throws InterruptedException {
        synchronized (monitor) {
            while (stack.isEmpty()) {
                monitor.wait();  // Ждём, пока что-то появится
            }
            return stack.pop();
        }
    }
}

Итог

Монитор — это встроенный механизм в каждом объекте, который позволяет:

  • Захватывать эксклюзивный доступ (synchronized)
  • Ждать события (wait())
  • Пробуждать ждущие потоки (notify/notifyAll)

Правило: Монитор = объект, на котором вызвал synchronized/wait/notify.

В современном Java лучше использовать Lock и Condition из java.util.concurrent.locks, они безопаснее и гибче.