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

Как синхронизировать созданные объекты класса

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
Как синхронизировать созданные объекты класса | PrepBro