0Pricing
Java Academy · Урок

Ошибки: закрепление и ThreadLocals

Избегайте блокировки потоков-носителей

«Ошибки: закрепление и ThreadLocals» — бесплатный урок Java Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.

Что может пойти не так

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

В этом уроке объясняется, что это такое и как этого избежать.

Что такое закрепление

Закрепление происходит, когда виртуальный поток не может демонтироваться со своего потока-носителя во время блокировки. Поток OS остаётся занят, сводя на нет преимущество виртуальных потоков в масштабируемости.

Когда закреплено слишком много потоков-носителей, пропускная способность резко падает.

Причина: блокировка в синхронизированном коде

Классическая причина — блокировка внутри блока или метода synchronized. Удерживая монитор и переходя в заблокированное состояние, виртуальный поток остаётся закреплённым за своим потоком-носителем.

Примечание: в JDK 24+ (JEP 491) это ограничение в значительной степени устранено, но в Java 21 оно действительно представляет проблему.

Пример закрепления, которого следует избегать

Такой шаблон может привести к закреплению в Java 21: поток блокируется, удерживая монитор. Код по-прежнему работает правильно, но не масштабируется.

public class Main {
    static final Object lock = new Object();

    static void risky() {
        synchronized (lock) {
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(Main::risky);
        t.join();
        System.out.println("Done (but this blocked inside synchronized)");
    }
}

Исправление: ReentrantLock

Заменяйте synchronized на ReentrantLock, если критическая секция может блокироваться. API блокировок хорошо подходят для виртуальных потоков и позволяют им демонтироваться.

import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static final ReentrantLock lock = new ReentrantLock();

    static void safe() {
        lock.lock();
        try {
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(Main::safe);
        t.join();
        System.out.println("Done without pinning");
    }
}

Диагностика закрепления

Можно попросить JVM выводить трассировку стека при каждом возникновении закрепления, запустив программу с параметром:

  • -Djdk.tracePinnedThreads=full для полных трассировок
  • -Djdk.tracePinnedThreads=short для однострочных сообщений

Это помогает найти проблемные участки кода synchronized.

Использование ThreadLocals становится дорогим

ThreadLocal хранит состояние для каждого потока. При наличии нескольких платформенных потоков это нормально. Но при миллионах виртуальных потоков, каждый из которых хранит собственную копию, потребление памяти резко возрастает.

Проверьте используемые библиотеки: кэширование, форматирование и объекты контекста часто скрывают ThreadLocals.

ThreadLocal по-прежнему работает

ThreadLocal не запрещён, просто используйте его умеренно. Каждый виртуальный поток получает собственное значение, как и ожидается.

public class Main {
    static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t = Thread.ofVirtual().start(() -> {
            CONTEXT.set("request-42");
            System.out.println("Context: " + CONTEXT.get());
            CONTEXT.remove();
        });
        t.join();
    }
}

Предпочитайте ScopedValue

В Java появился ScopedValue как более лёгкая альтернатива для передачи неизменяемых данных с ограниченным временем жизни. Он позволяет избежать затрат на изменяемое хранилище для каждого потока, характерное для ThreadLocal, и хорошо соответствует модели структурированной конкурентности.

Если нужно лишь передать контекст только для чтения вниз по цепочке вызовов, выбирайте его.

Не объединяйте в пул

Ещё один незаметный подводный камень — считать виртуальные потоки дефицитным ресурсом и объединять их в пул. Это вновь создаёт конкуренцию и приводит к утечке данных ThreadLocal между задачами.

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

Контрольный список для масштабирования

Перед выпуском виртуальных потоков в рабочую среду:

  • Замените блокирующий synchronized на ReentrantLock
  • Включите jdk.tracePinnedThreads при тестировании
  • Сведите к минимуму использование ThreadLocal; предпочитайте ScopedValue
  • Никогда не объединяйте виртуальные потоки в пул

Быстрая проверка

Определите безопасную замену блокирующему синхронизированному блоку.

Итоги

Вы узнали об основных подводных камнях:

  • Закрепление: блокировка внутри synchronized занимает поток-носитель; исправление — ReentrantLock
  • Диагностируйте проблему с помощью -Djdk.tracePinnedThreads
  • Затраты на ThreadLocal растут вместе с количеством потоков; предпочитайте ScopedValue
  • Никогда не объединяйте виртуальные потоки в пул

На этом курс «Виртуальные потоки» завершён.

Часто задаваемые вопросы

Урок «Ошибки: закрепление и ThreadLocals» бесплатный?

Да — полный текст урока «Ошибки: закрепление и ThreadLocals» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.

Чему я научусь в уроке «Ошибки: закрепление и ThreadLocals»?

Избегайте блокировки потоков-носителей Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Java Academy?

Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Ошибки: закрепление и ThreadLocals»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Java Academy?

Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Что такое виртуальные потоки
  2. Создание виртуальных потоков
  3. Платформенные и виртуальные потоки
  4. Ошибки: закрепление и ThreadLocals
← Назад к Java Academy