Skip to main content
    Обратно към Блога
    JavaМиграцияКорпоративен

    Миграция към Java 21: Ръководство за корпоративни екипи

    Как мигрирахме Olympus Mobility от Java 17 към 21: виртуални нишки, клопката със закотвянето, pattern matching и поетапен план за миграция.

    15 февруари 2026 г.6 мин четене
    D

    Dimitar Gochev

    CEO & R&D Team Lead

    Защо да мигрирате към Java 21?

    Java 21 е версия с дългосрочна поддръжка (LTS) и носи възможности, които корпоративните екипи трудно могат да подминат. От виртуалните нишки, които значително опростяват паралелната обработка, до pattern matching, който прави кода по-изразителен - Java 21 е най-голямата стъпка напред след Java 8.

    В Dinix Software наскоро завършихме пълна миграция на платформата Olympus Mobility - система, обслужваща над 500 000 потребители - от Java 17 към Java 21. Ето какво научихме.

    Виртуалните нишки: истинският пробив

    Виртуалните нишки (Project Loom, JEP 444) са може би най-значимата промяна в Java 21. С тях можете да създавате милиони леки нишки без режийните разходи на класическите платформени нишки. За приложения, ограничени от вход-изход - а такива са повечето корпоративни бекенд системи - това означава чувствително по-висока пропускателна способност при по-прост код.

    Важното е, че моделът на програмиране остава същият. Продължавате да пишете обикновен блокиращ код, а средата за изпълнение освобождава носещата нишка, докато виртуалната чака:

    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (var providerId : providerIds) {
            executor.submit(() -> client.fetchAvailability(providerId));
        }
    }

    В Spring Boot 3.2 и по-нови версии целият уеб слой се прехвърля към виртуални нишки с едно-единствено свойство:

    spring.threads.virtual.enabled=true

    При миграцията на Olympus Mobility преминаването към виртуални нишки за нашите REST API handler-и доведе до трикратно подобрение в обработката на едновременни заявки, без каквито и да е промени в архитектурата.

    Проблемът със закотвянето, за който никой не предупреждава

    Виртуалните нишки се създават евтино, но блокирането им не е безплатно. Когато виртуална нишка блокира в synchronized блок, тя закотвя (pinning) носещата платформена нишка и планировчикът не може да я преизползва, докато трае изчакването. Закотвят ли се достатъчно носещи нишки, пропускателната способност пада до нивото на класическите нишки.

    Решението е механично: навсякъде, където се извършва вход-изход, заменете synchronized с ReentrantLock.

    private final ReentrantLock lock = new ReentrantLock();
    
    void recordTrip(Trip trip) {
        lock.lock();
        try {
            repository.save(trip);   // блокиращ вход-изход, нишката може да се освободи
        } finally {
            lock.unlock();
        }
    }

    Пускайте натоварващите тестове с -Djdk.tracePinnedThreads=full, за да откриете проблемните места, преди да го направи продукцията. По-новите версии на JDK намалиха този проблем значително, но в 21 той е напълно реален и трябва да го заложите в дизайна.

    Pattern matching, record patterns и sealed класове

    Pattern matching за switch (JEP 441) и record patterns (JEP 440) заедно правят възможен по-функционален стил на писане в Java. Оказаха се особено полезни в домейн логиката ни, където сложните бизнес правила станаха осезаемо по-четими.

    sealed interface FareEvent permits TripStarted, TripEnded, TripCancelled {}
    
    String describe(FareEvent event) {
        return switch (event) {
            case TripStarted(var id, var at)       -> "trip %s started at %s".formatted(id, at);
            case TripEnded(var id, var cost)       -> "trip %s cost %s".formatted(id, cost);
            case TripCancelled c when c.refunded() -> "refunded cancellation";
            case TripCancelled c                   -> "cancellation, no refund";
        };
    }

    Компилаторът следи за изчерпателност. Добавите ли нов разрешен подтип, всеки switch, който не го обработва, спира да се компилира - така цял клас грешки от runtime се превръщат в грешки при компилация.

    Sequenced Collections

    По-дребна промяна с осезаема полза в ежедневието: SequencedCollection (JEP 431) най-после дава на подредените колекции общ начин да работят с двата си края.

    var first = trips.getFirst();
    var last  = trips.getLast();
    var newestFirst = trips.reversed();

    Край на list.get(list.size() - 1) и на факта, че при LinkedHashSet изобщо нямаше как да поискате последния елемент.

    Generational ZGC

    Generational ZGC (JEP 439) събира младите обекти отделно, а точно такъв е профилът на типичното уеб натоварване, където повечето обекти живеят колкото една заявка. Ако работите с големи heap-ове и държите на забавянето при най-бавните заявки, струва си да замерите с:

    -XX:+UseZGC -XX:+ZGenerational

    Мерете преди и след, при вашия собствен трафик. Изборът на garbage collector зависи от натоварването и G1 по подразбиране остава напълно разумен избор за много услуги.

    Стратегия за миграция: поетапният подход

    Препоръчваме поетапна миграция:

    1. Обновете първо build инструментите (Maven/Gradle) за поддръжка на Java 21
    2. Пуснете съществуващия набор от тестове - по-голямата част от кода ще работи без промени
    3. Отстранете употребите на остарели API-та, които компилаторът маркира
    4. Постепенно въвеждайте новите възможности в новия код
    5. Рефакторирайте съществуващия код там, където новите възможности носят яснота

    Направете стъпки 1-3 в един pull request, което променя поведението възможно най-малко. Ако обновяването на средата пътува отделно от въвеждането на новите езикови възможности, при регресия в продукцията веднага знаете кое от двете я е причинило.

    Често срещани клопки

    Внимавайте за следното по време на миграцията:

    • Някои библиотеки на трети страни още може да не поддържат Java 21 - проверете съвместимостта отрано
    • Виртуалните нишки се държат различно със synchronized блокове - за операции с вход-изход предпочитайте ReentrantLock
    • Ако използвате рефлексия интензивно, прегледайте новите ограничения за достъп до модулите
    • Всичко, което рови във вътрешностите на JDK - по-стари версии на Lombok, Mockito, bytecode агенти, някои APM колектори - се чупи първо, затова ги обновете преди самата среда
    • Пуловете от нишки, оразмерени за платформени нишки, вече вредят: фиксиран пул от 200 сега е таван от 200, а не предпазна мярка

    Ами Java 25?

    Java 25 е по-новата LTS версия и за проект от нулата изборът е очевиден. При заварена система сметката е друга: зад Java 21 стоят години поддръжка от библиотеките, всички основни фреймуърци стъпват на нея, а пътят от 17 към 21 е добре утъпкан. Преминаването 17 → 21 сега и 21 → 25 по-късно обикновено носи по-малко риск от един голям скок, защото на всяка стъпка зависимостите ви остават на версии, които други вече са пуснали в продукция.

    Заключение

    Миграцията към Java 21 си заслужава инвестицията. Само подобренията в производителността от виртуалните нишки оправдават усилието, а по-доброто изживяване при разработка с pattern matching и sealed класове прави кодовата ви база по-поддържана за години напред.

    Ако обмисляте обновяване на JDK в по-голяма система, екипите ни за разработка на софтуер и IT консултации вършат точно такава работа по миграции.