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

Что такое синхронизация?

1.7 Middle🔥 191 комментариев
#Многопоточность

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

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

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

# Синхронизация в Java: Многопоточность и потокобезопасность

Синхронизация (synchronization) — это механизм контроля доступа к общим ресурсам (переменным, объектам) в многопоточной среде. Она гарантирует, что только один поток может выполнять критический раздел кода одновременно, предотвращая race conditions и обеспечивая consistency данных.

Проблема: Race Condition

// ❌ Без синхронизации — race condition
public class Counter {
    private int count = 0;  // Общий ресурс
    
    public void increment() {
        count++;  // НЕ атомарная операция!
        // В реальности это: read -> increment -> write
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(counter.count);  // Ожидаем 20000
        // Но выведет что-то вроде: 15843, 18421, 19997 — НЕПРАВИЛЬНО!
    }
}

// Почему происходит race condition:
// Thread 1: read count=5 -> increment -> write count=6
// Thread 2: read count=5 -> increment -> write count=6
//           (прочитал старое значение, пока Thread 1 его обновлял)

Решение 1: synchronized методы

// ✅ С синхронизацией
public class SynchronizedCounter {
    private int count = 0;
    
    // synchronized гарантирует, что одновременно в методе
    // может находиться только один поток
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter counter = new SynchronizedCounter();
        
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(counter.getCount());  // 20000 ✅ ПРАВИЛЬНО!
    }
}

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

public synchronized void method() {
    // Когда поток входит в synchronized метод:
    // 1. Пытается захватить монитор объекта (mutex/lock)
    // 2. Если другой поток его держит — ждёт в очереди
    // 3. Выполняет код метода
    // 4. Освобождает монитор при выходе
}

// Это эквивалентно:
public void method() {
    synchronized(this) {
        // Критический раздел
    }
}

Решение 2: synchronized блоки (более гибко)

public class Counter {
    private int count = 0;
    private Object lock = new Object();  // Отдельный объект-монитор
    
    // Синхронизируем только нужную часть
    public void increment() {
        // Обработка без синхронизации (быстро)
        int temp = calculateValue();
        
        // Синхронизируем только критический раздел
        synchronized(lock) {
            count += temp;
        }
        
        // Больше кода без синхронизации
        doSomethingElse();
    }
    
    public int getCount() {
        synchronized(lock) {
            return count;
        }
    }
    
    private int calculateValue() { return 1; }
    private void doSomethingElse() { }
}

Решение 3: Современные инструменты

AtomicInteger (рекомендуется)

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Атомарная операция
    }
    
    public int getCount() {
        return count.get();
    }
}

ReentrantLock (более гибкий)

import java.util.concurrent.locks.ReentrantLock;

public class LockCounter {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // Важно разблокировать даже при исключении
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

// С try-with-resources (если использовать ReadWriteLock)
try(var ignored = lock.lock()) {
    count++;
}

Visibility: volatile

Атомарность ≠ Видимость

// ❌ Проблема видимости
public class VisibilityProblem {
    private boolean flag = false;
    
    public void thread1() {
        // Поток 1 может кэшировать значение flag
        while(!flag) {
            // Может бесконечно ждать, хотя flag изменилась
        }
    }
    
    public void thread2() {
        flag = true;  // Изменение может быть не видно thread1
    }
}

// ✅ Решение: volatile
public class VisibilityFixed {
    private volatile boolean flag = false;  // Видимо между потоками
    
    public void thread1() {
        while(!flag) {
            // Будет видеть изменения из thread2
        }
    }
    
    public void thread2() {
        flag = true;  // Сразу видимо всем потокам
    }
}

Memory Model: Happens-Before

// synchronized гарантирует happens-before отношение
public class HappensBefore {
    private int x = 0;
    private boolean ready = false;
    
    public synchronized void write() {
        x = 42;
        ready = true;
        // ← synchronized гарантирует:
        // все записи в памяти упорядочены
    }
    
    public synchronized void read() {
        if(ready) {
            System.out.println(x);  // Гарантированно 42, не 0
            // synchronized гарантирует видимость
        }
    }
}

Дедлоки: опасность синхронизации

// ❌ Дедлок
public class DeadlockExample {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void method1() {
        synchronized(lock1) {
            // Поток A здесь
            try { Thread.sleep(100); } catch(InterruptedException e) {}
            synchronized(lock2) {
                // Ждёт lock2...
            }
        }
    }
    
    public void method2() {
        synchronized(lock2) {
            // Поток B здесь
            try { Thread.sleep(100); } catch(InterruptedException e) {}
            synchronized(lock1) {
                // Ждёт lock1... ДЕДЛОК!
                // Поток A ждёт lock2 (которую держит B)
                // Поток B ждёт lock1 (которую держит A)
            }
        }
    }
}

// ✅ Правило: всегда захватывай монаторы в одинаковом порядке
public class NoDeadlock {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    // Всегда lock1 -> lock2
    public void method1() {
        synchronized(lock1) {
            synchronized(lock2) {
                // Безопасно
            }
        }
    }
    
    public void method2() {
        synchronized(lock1) {  // Сначала lock1
            synchronized(lock2) {  // Потом lock2
                // Безопасно
            }
        }
    }
}

Сравнение подходов

ПодходПреимуществаНедостаткиКогда использовать
synchronizedПростой, встроенныйГрубый, может быть медленнымПростые случаи
synchronized блокГибче, быстрееТребует аккуратностиКритические секции
AtomicIntegerБыстро, modernТолько для чиселСчётчики, флаги
ReentrantLockОчень гибко (tryLock, conditions)Сложнее, нужно unlockСложные сценарии
ReadWriteLockМного читателейБолее сложноМного read, мало write

Thread-safe Collections

// ❌ Не безопасно
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// Если несколько потоков обращаются одновременно — race condition

// ✅ Безопасно (Java 8+)
List<String> list = Collections.synchronizedList(new ArrayList<>());
// или (лучше)
List<String> list = new CopyOnWriteArrayList<>();

// ✅ Безопасно
Map<String, Integer> map = new ConcurrentHashMap<>();
// или
Set<String> set = Collections.newSetFromMap(new ConcurrentHashMap<>());

Best Practices

// 1. Минимизируй critical section
public synchronized void badMethod() {
    // Много кода здесь
    doExpensiveCalculation();
    updateDatabase();
    log();
}

// ✅ Лучше
public void goodMethod() {
    int result = doExpensiveCalculation();  // Без синхронизации
    synchronized(this) {  // Только нужное
        data = result;
    }
    updateDatabase();  // Без синхронизации
    log();  // Без синхронизации
}

// 2. Предпочитай modern инструменты
// ❌ synchronized int count = 0;
// ✅ AtomicInteger count = new AtomicInteger(0);

// 3. Избегай вложенных блокировок
// ❌ synchronized(lock1) { synchronized(lock2) { ... } }
// ✅ synchronized(lock1) { ... }

// 4. Используй volatile для флагов видимости
private volatile boolean shutdownRequested = false;

// 5. Документируй какие поля защищены
private int count;  // Защищено lock'ом
private final AtomicInteger atomic = new AtomicInteger();

Заключение

  1. Синхронизация — необходимо для безопасной многопоточности
  2. synchronized — простой встроенный механизм
  3. Atomics — современный и быстрый способ
  4. Locks — гибкий, но требует аккуратности
  5. volatile — для видимости, не для атомарности
  6. Дедлоки — всегда захватывай в одном порядке
  7. ConcurrentCollections — используй готовые thread-safe структуры
Что такое синхронизация? | PrepBro