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

Как управлять монитором

2.3 Middle🔥 161 комментариев
#JVM и управление памятью#Многопоточность#Основы Java

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

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

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

Управление мониторами в Java (Synchronization)

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

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

В Java каждый объект имеет встроенный lock (замок), называемый монитором:

Object obj = new Object();
// У obj есть собственный монитор
// Только один поток может держать lock на этом мониторе одновременно

Способ 1: synchronized блок

Это наиболее явный и контролируемый способ:

public class Counter {
    private int count = 0;
    private final Object lock = new Object();  // Явный монитор
    
    public void increment() {
        synchronized (lock) {  // Захватываем монитор
            count++;           // Критическая секция
        }                       // Освобождаем монитор
    }
    
    public int getValue() {
        synchronized (lock) {
            return count;
        }
    }
}

Как работает:

  1. Поток пытается захватить lock на мониторе
  2. Если lock свободен — захватывает и входит в блок
  3. Если lock занят — поток ждёт (блокируется)
  4. После выхода из блока — lock освобождается
  5. Другие потоки получают возможность захватить lock

Видимость памяти (Memory Visibility)

Важный эффект synchronized:

class Account {
    private int balance = 0;
    
    synchronized void deposit(int amount) {
        balance += amount;  // Видно всем потокам после synchronized
    }
    
    synchronized int getBalance() {
        return balance;     // Читаем самое актуальное значение
    }
}

Synchronized гарантирует:

  • Mutual Exclusion — только один поток в критической секции
  • Memory Visibility — изменения видны всем потокам

Способ 2: synchronized метод

Удобнее для методов:

public class Counter {
    private int count = 0;
    
    // synchronized на весь метод
    public synchronized void increment() {
        count++;
    }
    
    // Эквивалент:
    // public void increment() {
    //     synchronized (this) {
    //         count++;
    //     }
    // }
}

Для экземплярного метода — lock на самом объекте (this).

Для статического метода — lock на Class объекте:

public class Config {
    private static int globalCounter = 0;
    
    public synchronized static void increment() {
        globalCounter++;  // Lock на Config.class
    }
    
    // Эквивалент:
    // public static void increment() {
    //     synchronized (Config.class) {
    //         globalCounter++;
    //     }
    // }
}

Способ 3: Явное указание монитора

Для лучшего контроля используй разные мониторы:

public class Bank {
    private int account1 = 0;
    private int account2 = 0;
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void transfer(int amount) {
        synchronized (lock1) {
            account1 -= amount;
        }
        synchronized (lock2) {
            account2 += amount;
        }
    }
}

Это более гранулярное управление — два потока могут одновременно работать с account1 и account2.

Способ 4: ReentrantLock (Java 5+)

Больше контроля и функциональности:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();  // Захватить lock
        try {
            count++;   // Критическая секция
        } finally {
            lock.unlock();  // ВСЕГДА освобождаем
        }
    }
    
    public boolean tryIncrement() {
        if (lock.tryLock()) {  // Попытка без ожидания
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;  // Lock занят
    }
    
    public boolean tryIncrementWithTimeout() throws InterruptedException {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {  // Ждём 1 сек
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}

Преимущества ReentrantLock:

  • Timeout при захвате
  • Можно прервать ждущий поток (interruptible)
  • Можно проверить состояние lock
  • Гибче для сложных случаев

Способ 5: ReadWriteLock

Для сценариев "много читающих, мало пишущих":

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CachedData {
    private String data;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public String read() {
        lock.readLock().lock();
        try {
            return data;  // Несколько потоков могут читать одновременно
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void write(String newData) {
        lock.writeLock().lock();
        try {
            data = newData;  // Исключительный доступ
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Способ 6: synchronized Collections

Для работы с коллекциями:

List<String> list = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());

// Но обычно используют ConcurrentHashMap и другие concurrent классы
import java.util.concurrent.*;

ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

Проблемы и лучшие практики

Проблема 1: Deadlock

Плохо:

class Account {
    synchronized void transferTo(Account other, int amount) {
        this.balance -= amount;
        other.balance += amount;  // Может произойти deadlock!
    }
}

// Thread 1: account1.transferTo(account2, 100)
// Thread 2: account2.transferTo(account1, 100)
// Deadlock!

Хорошо:

class Account {
    private static final Lock globalLock = new ReentrantLock();
    
    void transferTo(Account other, int amount) {
        globalLock.lock();
        try {
            this.balance -= amount;
            other.balance += amount;
        } finally {
            globalLock.unlock();
        }
    }
}

Проблема 2: Holding locks слишком долго

Плохо:

public synchronized void process() {
    data = fetch();      // Долгая I/O операция
    data.process();      // Обработка
    save(data);         // Ещё долгая операция
}

Хорошо:

public void process() {
    Data data = fetch();  // Без lock
    data.process();       // Без lock
    synchronized (this) {
        this.data = data;  // Lock только для критического участка
    }
    save(data);          // Без lock
}

Проблема 3: Race conditions в проверках

Плохо:

if (count < MAX) {              // Проверка
    count++;                     // Действие
}                                // Race condition!

Хорошо:

synchronized(lock) {
    if (count < MAX) {           // Проверка
        count++;                  // Действие
    }                             // Всё атомарно
}

Best Practices

  1. Используй synchronized для простых случаев — встроена в язык
  2. Используй ReentrantLock когда нужна гибкость — timeout, interruptible
  3. Держи критические секции малыми — меньше блокировок
  4. Всегда используй finally при ReentrantLock
  5. Избегай вложенности мониторов — deadlock риск
  6. Используй concurrent классы — лучше чем Collections.synchronized
  7. Помни про memory visibility — synchronized гарантирует её
  8. Не вызывай wait() без причины — используй современные await/signal