JavaRush /Курсы /JAVA 25 SELF /Разбор типичных ошибок при синхронизации

Разбор типичных ошибок при синхронизации

JAVA 25 SELF
52 уровень , 4 лекция
Открыта

1. Забытый unlock/release: ловушка для невнимательных

Одна из самых коварных ошибок при использовании современных инструментов синхронизации, таких как ReentrantLock или Semaphore, — забыть вызвать unlock() или release(). Если вы не освободите блокировку, то другие потоки будут ждать её освобождения... вечно. Программа зависнет, и вы будете долго смотреть на экран, пытаясь понять, почему ничего не происходит.

Рассмотрим пример с ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        // Ой! Забыли unlock() — теперь все зависнут!
        count++;
    }
}

Всё выглядит невинно, но если вызвать increment() несколько раз из разных потоков, после первого вызова остальные потоки будут ждать освобождения блокировки бесконечно.

Во избежание этой ситуации используйте конструкцию try-finally:

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

Теперь, даже если в середине метода произойдёт исключение, блокировка будет гарантированно освобождена.

Это как если бы кто-то занял уборную (заперся изнутри), а потом забыл открыть дверь и вышел в окно. Остальные будут ждать, пока этот человек не выйдет... Не делайте так!

2. Синхронизация на неправильном объекте: «Ох, не туда замок повесил!»

В Java ключевое слово synchronized может блокировать доступ к какому-то объекту. Но если вы выберете неправильный объект для блокировки, синхронизация не сработает так, как вы ожидаете.

Ошибка №1: синхронизация на локальной переменной

public void doSomething() {
    Object lock = new Object();
    synchronized (lock) {
        // Каждый раз новый объект — никакой синхронизации!
        // Потоки не ждут друг друга.
        // Критическая секция не защищена!
    }
}

Здесь каждый поток создаёт свой собственный объект lock. В результате никакой реальной блокировки не происходит — потоки проходят в критическую секцию одновременно.

Правильно:

private final Object lock = new Object();

public void doSomething() {
    synchronized (lock) {
        // Теперь все потоки используют один и тот же объект lock
        // и реально ждут друг друга.
    }
}

Ошибка №2: синхронизация на строковом литерале

public void doSomething() {
    synchronized ("lock") {
        // Строковые литералы интернированы: разные части программы могут
        // случайно синхронизироваться на одной и той же строке!
    }
}

Вывод:
Синхронизируйтесь только на приватных, специально созданных для этого объектах, которые не используются нигде ещё.

3. Двойная блокировка (deadlock): «Ты мне — я тебе, и оба встали»

Deadlock (взаимная блокировка) — это классика жанра. Два (или более) потока захватывают поочерёдно разные блокировки и ждут друг друга, пока программа не встанет колом.

Пример:

public class DeadlockExample {
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            // Немного подождём для чистоты эксперимента
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockB) {
                // ...
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockA) {
                // ...
            }
        }
    }
}

Если один поток вызовет method1(), а другой — method2(), то первый поток захватит lockA и будет ждать lockB, а второй — наоборот. В результате оба будут ждать друг друга до скончания веков.

Как избежать?

  • Всегда захватывайте блокировки в одном и том же порядке во всех потоках.
  • Минимизируйте количество одновременно удерживаемых блокировок.
  • Используйте средства диагностики (например, jstack), если программа зависла.

Аналогия:
Это как если бы два человека встретились в узком коридоре, и каждый решил уступить дорогу, но только если другой сначала уступит ему. В итоге оба стоят и ждут, пока кто-то первый не сдастся.

4. Избыточная синхронизация: «Лучше перебдеть, чем недобдеть?» — не всегда!

Иногда разработчики, опасаясь ошибок, синхронизируют всё подряд. В результате производительность падает, а пользы — ноль.

Пример:

public synchronized void add(int value) {
    // Здесь только одна строчка, которая не требует синхронизации!
    System.out.println("Добавлено: " + value);
}

В этом случае синхронизация не нужна: вывод на экран через System.out.println уже потокобезопасен, а сам метод не работает с общими ресурсами.

Где это критично?
Если вы синхронизируете методы, которые вызываются часто и не требуют защиты, вы резко снижаете производительность программы. Потоки выстраиваются в очередь, хотя могли бы работать параллельно.

Best practice:
Синхронизируйте только то, что действительно необходимо. Критическая секция должна быть как можно меньше.

5. Неправильное использование volatile: «Видимость есть, атомарности нет!»

Модификатор volatile в Java гарантирует, что изменения переменной будут видны всем потокам. Но он не гарантирует атомарность операций.

Ошибка:

private volatile int counter = 0;

public void increment() {
    counter++; // Не атомарно!
}

Операция counter++ состоит из чтения значения, увеличения и записи обратно. Если два потока одновременно выполняют этот код, итоговое значение может быть меньше ожидаемого.

Правильно:
Для атомарных операций используйте synchronized, AtomicInteger или другие потокобезопасные классы.

import java.util.concurrent.atomic.AtomicInteger;

private final AtomicInteger counter = new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

Когда использовать volatile?
Для простых флагов (например, «завершить работу»), когда не требуется атомарность.

1
Задача
JAVA 25 SELF, 52 уровень, 4 лекция
Недоступна
Надёжная система учёта транзакций 💳
Надёжная система учёта транзакций 💳
1
Задача
JAVA 25 SELF, 52 уровень, 4 лекция
Недоступна
Централизованный офисный принтер 🖨️
Централизованный офисный принтер 🖨️
1
Опрос
Синхронизация потоков, 52 уровень, 4 лекция
Недоступен
Синхронизация потоков
Синхронизация потоков
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ