Как управлять монитором
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Управление мониторами в 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;
}
}
}
Как работает:
- Поток пытается захватить lock на мониторе
- Если lock свободен — захватывает и входит в блок
- Если lock занят — поток ждёт (блокируется)
- После выхода из блока — lock освобождается
- Другие потоки получают возможность захватить 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
- Используй synchronized для простых случаев — встроена в язык
- Используй ReentrantLock когда нужна гибкость — timeout, interruptible
- Держи критические секции малыми — меньше блокировок
- Всегда используй finally при ReentrantLock
- Избегай вложенности мониторов — deadlock риск
- Используй concurrent классы — лучше чем Collections.synchronized
- Помни про memory visibility — synchronized гарантирует её
- Не вызывай wait() без причины — используй современные await/signal