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

Были ли проблемы с синхронизацией

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

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

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

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

# Проблемы с синхронизацией: мой опыт

Да, я сталкивался с проблемами синхронизации. Это одна из самых хитрых частей многопоточного программирования. Расскажу реальные случаи.

1. Race condition в кеше (самая распространённая)

Проблема, с которой столкнулся

// Этот код выглядит правильно, но скрывает race condition
public class UserCache {
    private Map<Long, User> cache = new HashMap<>();  // ❌ НЕ синхронизирован
    
    public User getUser(Long id) {
        if (!cache.containsKey(id)) {  // Проверка
            User user = fetchFromDB(id);
            cache.put(id, user);        // Запись
            return user;
        }
        return cache.get(id);           // Чтение
    }
}

// Сценарий:
Поток 1: cache.containsKey(1) → false
Поток 2: cache.containsKey(1) → false  (обе проверили одновременно!)
Поток 1: fetchFromDB(1) → User A
Поток 2: fetchFromDB(1) → User A       (оба обращались в БД!)
Поток 1: cache.put(1, User A)
Поток 2: cache.put(1, User A)         (оба писали в кеш)

// Результат: double fetching (неэффективно, но работает)

Решение

// Способ 1: synchronized
public class UserCache {
    private final Map<Long, User> cache = new ConcurrentHashMap<>();
    
    public User getUser(Long id) {
        return cache.computeIfAbsent(id, k -> fetchFromDB(k));
        // Atomic get-if-absent-put
    }
}

// Способ 2: Double-checked locking (когда computeIfAbsent недоступен)
private final Object lock = new Object();

public User getUser(Long id) {
    if (!cache.containsKey(id)) {  // First check (без блокировки)
        synchronized (lock) {
            if (!cache.containsKey(id)) {  // Second check (с блокировкой)
                User user = fetchFromDB(id);
                cache.put(id, user);
            }
        }
    }
    return cache.get(id);
}

2. Deadlock при обновлении двух объектов

Реальная история

public class MoneyTransfer {
    public synchronized void transfer(Account from, Account to, int amount) {
        from.withdraw(amount);    // synchronized на from
        to.deposit(amount);       // synchronized на to
    }
}

// Сценарий deadlock:
Поток 1: transfer(acc1, acc2, 100)   // Захватывает lock на acc1
Поток 2: transfer(acc2, acc1, 200)   // Захватывает lock на acc2

Поток 1: Ждёт lock на acc2 (заблокирован потоком 2)
Поток 2: Ждёт lock на acc1 (заблокирован потоком 1)

// DEADLOCK! Оба потока ждут друг друга

Решение

public class MoneyTransfer {
    // Всегда захватывай в одном порядке (по ID)
    public void transfer(Account from, Account to, int amount) {
        if (from.getId() < to.getId()) {
            synchronized (from) {
                synchronized (to) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        } else {
            synchronized (to) {
                synchronized (from) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        }
    }
    
    // ИЛИ используй ReentrantLock
    private final Lock lock = new ReentrantLock();
    
    public void transferWithLock(Account from, Account to, int amount) {
        lock.lock();
        try {
            from.withdraw(amount);
            to.deposit(amount);
        } finally {
            lock.unlock();
        }
    }
}

3. False sharing (выравнивание кеша процессора)

Проблема

public class FalseSharing {
    public static class Data {
        public long value = 0;  // Может быть в одной cache line
    }
    
    public static void main(String[] args) throws InterruptedException {
        Data[] data = new Data[2];
        data[0] = new Data();
        data[1] = new Data();
        
        long start = System.nanoTime();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100_000_000; i++) {
                data[0].value += 1;  // Пишет в data[0]
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100_000_000; i++) {
                data[1].value += 1;  // Пишет в data[1]
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        long time = System.nanoTime() - start;
        System.out.println("Time: " + time + "ns");  // ~5 сек (медленно!)
    }
}

// Проблема: data[0] и data[1] находятся в одной CPU cache line
// Когда t1 пишет в data[0], процессор инвалидирует cache line
// и t2 должен перечитать из памяти (очень медленно)

Решение (Padding)

public static class Data {
    public long value = 0;
    // Добавляем padding, чтобы data[0] и data[1] были в разных cache lines
    public long p1, p2, p3, p4, p5, p6, p7 = 0;  // 7 * 8 bytes = 56 bytes padding
}

// Time: ~1 сек (в 5 раз быстрее!)

// Java 9+: используй @Contended
@jdk.internal.vm.annotation.Contended
public static class BetterData {
    public long value = 0;
}

4. Memory visibility issue (видимость памяти)

Случай из практики

public class VisibilityProblem {
    private int flag = 0;
    
    public void writer() {
        flag = 1;  // Пишу в flag
    }
    
    public void reader() {
        while (flag == 0) {  // ❌ Может зависнуть!
            // На многоядерной машине поток может не видеть изменение flag
        }
        System.out.println("Flag is 1!");
    }
}

// Сценарий:
Поток 1: вызывает reader()
         while (flag == 0) ... // кеширует flag в L1 кеш процессора
Поток 2: вызывает writer()
         flag = 1;  // пишет в main memory
Поток 1: всё ещё видит flag = 0 (из кеша!)
         бесконечный loop (ЗАВИСЛО!)

Решение: volatile

public class VisibilityFixed {
    private volatile int flag = 0;  // ← VOLATILE
    
    public void writer() {
        flag = 1;  // Пишется в main memory СРАЗУ
    }
    
    public void reader() {
        while (flag == 0) {  // Читается из main memory КАЖДЫЙ раз
        }
        System.out.println("Flag is 1!");
    }
}

5. ConcurrentModificationException (изменение во время итерации)

Было в production

public class ConcurrentModificationIssue {
    private List<User> users = new ArrayList<>();
    
    public void process() {
        for (User user : users) {  // Iterator
            if (user.isInactive()) {
                users.remove(user);  // ❌ Modifying during iteration!
                // ConcurrentModificationException
            }
        }
    }
}

Решения

// Способ 1: Iterator
public void processWithIterator() {
    Iterator<User> it = users.iterator();
    while (it.hasNext()) {
        User user = it.next();
        if (user.isInactive()) {
            it.remove();  // ✅ Безопасное удаление
        }
    }
}

// Способ 2: Stream
public void processWithStream() {
    users = users.stream()
        .filter(u -> !u.isInactive())
        .collect(Collectors.toList());
}

// Способ 3: removeIf
public void processWithRemoveIf() {
    users.removeIf(User::isInactive);
}

6. Проблема с synchronized дорогостоящих операций

Была ошибка

public class SlowSynchronization {
    public synchronized void processLargeData(List<Data> data) {
        // Весь метод синхронизирован!
        List<Result> results = new ArrayList<>();
        for (Data d : data) {
            results.add(expensiveComputation(d));  // Медленная операция
        }
        saveToDatabase(results);  // Ещё медленнее
        // Весь этот процесс держит блокировку!
    }
}

// Проблема: другие потоки ждут, пока вычисление и БД закончатся

Решение

public class OptimizedSynchronization {
    private final List<Data> sharedData = Collections.synchronizedList(...);
    
    public void processLargeData(List<Data> data) {
        // 1. Только скопируем (нужна синхронизация)
        List<Data> localCopy;
        synchronized (this) {
            localCopy = new ArrayList<>(data);
        }
        
        // 2. Дорогую операцию делаем БЕЗ синхронизации
        List<Result> results = localCopy.stream()
            .map(this::expensiveComputation)
            .collect(Collectors.toList());
        
        // 3. Только сохранение нужна синхронизация
        synchronized (this) {
            saveToDatabase(results);
        }
    }
}

7. Проблемы с try-finally и блокировками

Была ошибка

lock.lock();  // Захватили блокировку
try {
    // Обработка
} catch (Exception e) {
    // ❌ Забыли unlock!
}
// lock.unlock();  // ← НЕ выполнится при исключении если нет finally

Правильно

lock.lock();
try {
    // Обработка
} finally {
    lock.unlock();  // ✅ ВСЕГДА выполнится
}

// ИЛИ в Java 9+
try (LockResource lr = new LockResource(lock)) {
    // Обработка
}  // Lock автоматически освобождается

8. Проблемы при миграции на многопроцессорную систему

Был опыт

// На одноядерной машине это работало
public class SequentialBuggyCode {
    private int counter = 0;
    
    public void increment() {
        counter++;  // В одном потоке работает
    }
}

// На многоядерной машине:
// - Потоки работают параллельно
// - counter++ может быть прерван
// - Получаем race condition

Решение

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();  // Атомарно

Мои выводы из опыта

  1. Многопоточность — source of bugs: хитрые, непредсказуемые, сложно воспроизвести

  2. Лучше избежать многопоточности:

    • Используй message queues (Kafka, RabbitMQ)
    • Используй actor models (Akka)
    • Делай один поток работает с данными
  3. Если многопоточность необходима:

    • Используй современные инструменты: ConcurrentHashMap, AtomicInteger, CountDownLatch
    • Избегай низкоуровневого synchronized
    • Используй ReentrantLock с try-finally
    • ВСЕГДА тестируй на многоядерных машинах
  4. Инструменты для поиска проблем:

    • ThreadSanitizer (в C++, аналогов в Java мало)
    • JUnit с @Repeat и stress testing
    • Java Flight Recorder для профилирования
    • Visualvm для анализа deadlock'ов
  5. Red flags:

    • Если видишь synchronized на методе → подозреваю проблемы
    • Если видишь вложенные synchronized → может быть deadlock
    • Если видишь while (true) { ... } с проверкой флага → visibility issue

Итог

Синхронизация — одна из самых сложных тем в Java. Мой совет:

  • Избегай её, пока можно
  • Когда нужна — используй современные инструменты
  • Пиши тесты на многопоточность
  • Будь осторожен с deadlock'ами и visibility проблемами
Были ли проблемы с синхронизацией | PrepBro