← Назад к вопросам
Как синхронизировать созданные объекты класса
1.8 Middle🔥 171 комментариев
#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как синхронизировать созданные объекты класса
Синхронизация объектов в Java — это механизм, который обеспечивает безопасный доступ к одному объекту из нескольких потоков одновременно. Это критично в многопоточных приложениях для избежания race conditions и гарантирования целостности данных.
Проблема: Race Condition
Без синхронизации несколько потоков могут одновременно изменять объект:
public class BankAccount {
private int balance = 1000;
// БЕЗ синхронизации - ОПАСНО!
public void withdraw(int amount) {
if (balance >= amount) {
// Race condition здесь!
// Два потока проверяют баланс одновременно
// Оба видят 1000, оба начинают вывод
balance = balance - amount;
}
}
public int getBalance() {
return balance; // Может читать грязное значение
}
}
// Сценарий race condition:
public class Main {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount(); // balance = 1000
// Два потока пытаются вывести деньги
Thread thread1 = new Thread(() -> account.withdraw(600));
Thread thread2 = new Thread(() -> account.withdraw(400));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// Ожидаемый результат: balance = 0
// Реальный результат: может быть 400 (race condition!)
System.out.println("Balance: " + account.getBalance());
}
}
// Что произошло:
// Время | Thread 1 | Thread 2 | balance
// T1 | read balance=1000 |
// T2 | | read balance=1000 |
// T3 | 1000-600=400 |
// T4 | | 1000-400=600 |
// T5 | write balance=400 |
// T6 | | write balance=600 | ← НЕПРАВИЛЬНО!
//
// Оба вывода прошли, но only 400 потеряно!
Решение 1: synchronized метод
Самый простой способ — синхронизировать весь метод:
public class SynchronizedBankAccount {
private int balance = 1000;
// ✅ synchronized - всё правильно
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance = balance - amount; // Атомарная операция
}
}
public synchronized int getBalance() {
return balance;
}
public synchronized void deposit(int amount) {
balance = balance + amount;
}
}
// Теперь доступ потокобезопасен:
public class Main {
public static void main(String[] args) throws InterruptedException {
SynchronizedBankAccount account = new SynchronizedBankAccount();
Thread thread1 = new Thread(() -> account.withdraw(600));
Thread thread2 = new Thread(() -> account.withdraw(400));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Balance: " + account.getBalance()); // Гарантирует 0
}
}
// Как работает synchronized:
// Каждый объект имеет свой MONITOR (внутренний замок)
// Когда поток входит в synchronized метод, он захватывает monitor
// Другие потоки ждут, пока monitor будет освобожден
Как работает synchronized изнутри
Визуализация:
┌─ BankAccount объект
│
├─ MONITOR (замок) ← Может владеть ТОЛЬКО один поток
│ │
│ └─ Thread 1 владеет (входит в withdraw)
│
├─ balance: 1000
└─ другие данные
Что происходит:
Так T1:
Thread 1 вызывает withdraw(600)
↓
Thread 1 захватывает MONITOR
↓
Thread 1 может работать с balance
Так T2:
Thread 2 вызывает withdraw(400)
↓
Thread 2 пытается захватить MONITOR
↓
MONITOR занят, Thread 2 ЖДЁТ
Так T3:
Thread 1 завершает withdraw
↓
Thread 1 освобождает MONITOR
↓
Thread 2 захватывает MONITOR
↓
Thread 2 начинает работать с balance
Решение 2: synchronized блок
Для большей контроля можно синхронизировать только часть метода:
public class BankAccount {
private int balance = 1000;
private Object lock = new Object(); // Явный замок
public void withdraw(int amount) {
System.out.println("Checking balance..."); // БЕЗ синхронизации
synchronized(lock) { // Синхронизация только критичной части
if (balance >= amount) {
balance = balance - amount; // Защищено
}
}
System.out.println("Withdrawal processed..."); // БЕЗ синхронизации
}
// Альтернатива: синхронизировать по самому объекту
public void deposit(int amount) {
synchronized(this) { // Синхронизация по текущему объекту
balance = balance + amount;
}
}
}
// synchronized(this) эквивалентен synchronized методу
public class Account {
private int balance = 0;
// Эти два варианта идентичны:
// Вариант 1:
public synchronized void withdraw1(int amount) {
balance -= amount;
}
// Вариант 2:
public void withdraw2(int amount) {
synchronized(this) {
balance -= amount;
}
}
}
Решение 3: volatile для простых переменных
Для примитивных типов можно использовать volatile вместо полной синхронизации:
public class VolatileBankAccount {
private volatile int balance = 1000; // Гарантирует видимость
// volatile гарантирует:
// 1. Все операции чтения/записи видны другим потокам немедленно
// 2. Нет переупорядочивания операций
public int getBalance() {
return balance; // Всегда читает свежее значение
}
public void setBalance(int newBalance) {
balance = newBalance; // Все потоки видят изменение
}
}
// Но НЕ подходит для операций read-modify-write:
public class BadExample {
private volatile int counter = 0;
public void increment() {
counter++; // ЭТО НЕ безопасно!
// Даже с volatile, это три операции: read, modify, write
// Race condition может случиться между ними
}
}
// Правильно:
public class GoodExample {
private volatile int counter = 0;
public synchronized void increment() {
counter++; // Теперь безопасно
}
}
Решение 4: AtomicInteger (современный подход)
Для операций над числами есть специальные классы:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicBankAccount {
private AtomicInteger balance = new AtomicInteger(1000);
public void withdraw(int amount) {
balance.addAndGet(-amount); // Атомарная операция
}
public int getBalance() {
return balance.get();
}
public void deposit(int amount) {
balance.addAndGet(amount);
}
}
// Другие Atomic классы:
AtomicLong atomicLong = new AtomicLong();
AtomicBoolean atomicBoolean = new AtomicBoolean();
AtomicReference<String> atomicRef = new AtomicReference<>();
// Доступные операции:
atomicLong.increment(); // ++
atomicLong.decrement(); // --
atomicLong.addAndGet(5); // += 5
atomicLong.getAndSet(10); // Получить и установить
atomicLong.compareAndSet(10, 20); // Compare and swap
Решение 5: ReentrantLock (явная синхронизация)
Для большего контроля можно использовать Lock API:
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
public class LockBankAccount {
private int balance = 1000;
private Lock lock = new ReentrantLock();
public void withdraw(int amount) {
lock.lock(); // Захватить замок
try {
if (balance >= amount) {
balance = balance - amount;
}
} finally {
lock.unlock(); // ВСЕГДА освободить замок
}
}
public void withdrawWithTimeout(int amount) throws InterruptedException {
// Попробовать получить замок с таймаутом
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
if (balance >= amount) {
balance = balance - amount;
}
} finally {
lock.unlock();
}
} else {
System.out.println("Не удалось получить доступ");
}
}
}
// Отличие от synchronized:
// - synchronized: автоматически освобождается
// - ReentrantLock: нужно вручную unlock в finally
// + ReentrantLock: поддерживает tryLock с таймаутом
// + ReentrantLock: можно уведомлять потоки
Решение 6: ReadWriteLock
Для разных прав доступа на чтение и запись:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CachedBankAccount {
private int balance = 1000;
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public int getBalance() {
rwLock.readLock().lock(); // Много потоков могут одновременно читать
try {
return balance;
} finally {
rwLock.readLock().unlock();
}
}
public void withdraw(int amount) {
rwLock.writeLock().lock(); // Только один поток может писать
try {
if (balance >= amount) {
balance = balance - amount;
}
} finally {
rwLock.writeLock().unlock();
}
}
}
// Пример параллельного доступа:
// T1: readLock ✓ (читает баланс)
// T2: readLock ✓ (читает баланс одновременно с T1)
// T3: writeLock ✗ ЖДЁТ (T1 и T2 должны закончить)
// T1,T2: readLock освобождены
// T3: writeLock ✓ (теперь может писать)
Сравнение подходов
| Подход | Простота | Производительность | Гибкость |
|---|---|---|---|
| synchronized метод | ✓✓✓ | Средняя | Низкая |
| synchronized блок | ✓✓ | Хорошая | Средняя |
| volatile | ✓✓✓ | Отличная | Низкая |
| AtomicInteger | ✓✓ | Отличная | Средняя |
| ReentrantLock | ✓ | Хорошая | Отличная |
| ReadWriteLock | ✗ | Хорошая | Отличная |
Лучшие практики
// 1. Используй synchronized для простых случаев
public class Simple {
private int value;
public synchronized void setValue(int v) {
value = v;
}
}
// 2. Используй volatile для примитивов
public class Flags {
private volatile boolean running = true;
}
// 3. Используй Atomic для чисел
public class Counter {
private AtomicLong count = new AtomicLong();
}
// 4. Избегай синхронизации в циклах
// ❌ ПЛОХО:
for (int i = 0; i < 1000; i++) {
synchronized(this) {
counter++;
}
}
// ✅ ХОРОШО:
synchronized(this) {
for (int i = 0; i < 1000; i++) {
counter++;
}
}
// 5. Используй Collections.synchronizedXxx() для коллекций
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// 6. Предпочитай ConcurrentHashMap для многопоточных операций
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
Резюме
- Синхронизация защищает объекты от race conditions в многопоточной среде
- synchronized методы/блоки: самый простой подход для критичных секций
- volatile: для видимости значений примитивных типов
- AtomicInteger/Long: для безопасных операций над числами
- ReentrantLock/ReadWriteLock: для большего контроля и гибкости
- Ключ: захватить замок перед модификацией, освободить в finally