JavaRush /Курсы /JAVA 25 SELF /synchronized, volatile: синтаксис, применение

synchronized, volatile: синтаксис, применение

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

1. Ключевое слово synchronized: зачем и как

В Java ключевое слово synchronized — это как табличка «Занято!» на двери уборной: пока один поток находится внутри «критической секции», остальные вежливо ждут своей очереди. Только когда первый выйдет, следующий сможет войти и выполнить свой код.

Синтаксис: блок и метод

Синхронизированный блок

synchronized (object) {
    // критическая секция
}
  • object — это любой объект, на котором вы хотите «повесить замок». Пока один поток выполняет этот блок, другие потоки, которые тоже хотят войти в блок с этим же объектом, будут ждать.

Синхронизированный метод

public synchronized void increment() {
    // критическая секция
}
  • Здесь «замок» вешается на сам объект (this). То есть только один поток за раз может выполнять любой синхронизированный метод этого объекта.

Статический синхронизированный метод

public static synchronized void foo() {
    // критическая секция
}
  • Здесь блокировка происходит на уровне класса (ClassName.class), а не конкретного объекта.

Как это работает под капотом

Когда поток входит в синхронизированный блок или метод, он захватывает «монитор» объекта (или класса для статических методов). Если монитор уже занят — поток ждёт. Как только монитор освобождается, следующий поток может войти.

2. Пример: инкремент счётчика с и без синхронизации

Без синхронизации

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Итоговое значение: " + counter.getCount());
    }
}

Ожидаемое значение: 2000
Реальное значение: может быть меньше (например, 1995, 1987...), и при каждом запуске — свой «сюрприз».

Почему? Потому что операция count++ не атомарна: она разбивается на три действия — прочитать значение, увеличить, записать обратно. Если два потока делают это одновременно, они могут «перетереть» результат друг друга.

Решение: synchronized

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Теперь только один поток за раз может выполнять метод increment(). Итоговое значение всегда будет 2000.

Альтернатива: синхронизированный блок

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

Результат будет тот же. Можно синхронизировать не весь метод, а только нужную часть.

3. Введение в «монитор объекта»

Монитор — это «замок», встроенный в каждый объект в Java. Когда вы пишете synchronized(object), поток пытается «запереть» этот объект. Если замок свободен — поток его получает, если нет — ждёт своей очереди. Как только поток выходит из блока, замок освобождается.

Важно! Если вы синхронизируете на разных объектах — потоки не будут ждать друг друга. Поэтому очень важно выбрать правильный объект для синхронизации.

Статические синхронизированные методы

Иногда общий ресурс — это не объект, а что-то общее для всех экземпляров класса (например, статическая переменная). В этом случае синхронизация должна быть на уровне класса.

public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

Это эквивалентно:

public static void increment() {
    synchronized (StaticCounter.class) {
        count++;
    }
}

Монитор вешается на объект класса (Class), а не на конкретный экземпляр.

4. Ключевое слово volatile: что это и зачем

Проблема видимости между потоками

В Java каждый поток может кэшировать значения переменных для ускорения работы. Это значит, что если один поток изменил переменную, другой поток может этого «не заметить», продолжая читать значение из своего локального кэша. Это особенно критично для флагов, которыми потоки сигнализируют друг другу.

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

Если переменная объявлена как volatile, это значит:

  • Все потоки всегда читают и пишут её только в основную память, минуя кэш.
  • Любое изменение переменной становится немедленно видно всем потокам.

Но! Операции с volatile сами по себе не атомарны (кроме простого чтения/записи примитивов наподобие boolean, int и т. п.). Если вы делаете что-то сложнее, чем присваивание — нужна синхронизация.

Пример: флаг завершения работы

public class Worker extends Thread {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // делаем что-то полезное
        }
        System.out.println("Поток завершён");
    }

    public void shutdown() {
        running = false;
    }
}
Worker w = new Worker();
w.start();
// ... через какое-то время
w.shutdown();

Без volatile поток может «не заметить» изменения флага и зациклиться навечно (особенно на многоядерных системах). С volatile — всё работает как надо.

5. Ограничения volatile: неатомарность

Многие новички думают: «Если сделать volatile int, то можно писать count++ и не париться». Увы, это не так:

private volatile int count = 0;

public void increment() {
    count++;
}

Ошибка! Операция count++ всё равно не атомарна — это три шага: (1) прочитать, (2) увеличить, (3) записать обратно. Если два потока одновременно прочитают одно и то же значение, оба увеличат его и оба запишут одинаковый результат — один инкремент «потеряется».

Вывод: volatile обеспечивает только видимость изменений, но не защищает от гонок при сложных операциях.

6. Когда использовать synchronized, а когда — volatile

  • volatile — когда у вас есть простой флаг (например, boolean), который один поток пишет, а другой читает. Пример: завершение работы потока, сигнализация о событии.
  • synchronized — когда нужно обеспечить атомарность сложных операций (например, инкремент, изменение нескольких переменных, работа со структурами данных).

Таблица для запоминания

Сценарий volatile synchronized
Передать сигнал между потоками
Атомарная операция (инкремент)
Множественные шаги в критической секции
Только видимость изменений

7. Типичные ошибки при использовании synchronized и volatile

Ошибка №1: Синхронизация на неправильном объекте. Если вы синхронизируете на локальной переменной или на разном объекте в каждом потоке — никакой защиты не получится.

Object lock = new Object();
synchronized (lock) {
    // ...
}

Если каждый поток создаёт свой lock — толку ноль. Нужна единая точка синхронизации, общий объект для всех потоков.

Ошибка №2: Ожидание атомарности от volatile. volatile гарантирует видимость, а не атомарность. Операции наподобие count++ по-прежнему небезопасны без синхронизации.

Ошибка №3: Синхронизация слишком большой области кода. Если вы синхронизируете весь метод, а нужно — только одну строку, вы зря блокируете другие потоки и теряете производительность. Старайтесь уменьшать «критическую секцию».

Ошибка №4: Забыли сделать синхронизацию «статической» для статических данных. Если у вас статическая переменная, а вы синхронизируете на this, это не поможет. Для статических данных нужна синхронизация на уровне класса: synchronized(ClassName.class).

Ошибка №5: Синхронизация на строковом литерале. Синхронизация на строках опасна, потому что одинаковые литералы интернируются JVM. Можно случайно получить общую блокировку для разных частей программы.

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