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 уже потокобезпечне, а сам метод не працює зі спільними ресурсами.

Де це критично?
Якщо ви синхронізуєте методи, які викликаються часто й не потребують захисту, ви різко знижуєте продуктивність програми. Потоки шикуються в чергу, хоча могли б працювати паралельно.

Найкраща практика:
Синхронізуйте лише те, що справді необхідно. Критична секція має бути якомога меншою.

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
Опитування
Синхронізація потоків, рівень 52, лекція 4
Недоступний
Синхронізація потоків
Синхронізація потоків
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ