Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Показати більше📈 Аналітичний огляд Telegram-каналу Java: fill the gaps
Канал Java: fill the gaps (@java_fillthegaps) у мовному сегменті Російська є активним учасником. На даний момент спільнота об'єднує 12 549 підписників, посідаючи 10 120 місце в категорії Технології та додатки та 52 841 місце у регіоні Росія.
📊 Показники аудиторії та динаміка
З моменту свого створення невідомо, проект продемонстрував стрімке зростання, зібравши аудиторію у 12 549 підписників.
За останніми даними від 07 червня, 2026, канал демонструє стабільну активність. Хоча за останні 30 днів спостерігається зміна кількості учасників на -46, а за останні 24 години на 0, загальне охоплення залишається високим.
- Статус верифікації: Не верифікований
- Рівень залученості (ER): Середній показник залученості аудиторії становить 34.72%. Протягом перших 24 годин після публікації контент зазвичай збирає N/A% реакцій від загальної кількості підписників.
- Охоплення публікацій: В середньому кожен допис отримує 0 переглядів. Протягом першої доби публікація в середньому набирає 0 переглядів.
- Реакції та взаємодія: Аудиторія активно підтримує контент: середня кількість реакцій на один пост – 0.
- Тематичні інтереси: Контент зосереджений навколо ключових тем, таких як redis, hashmap, linkedhashmap, индекс, фича.
📝 Опис та контентна політика
Автор описує ресурс як майданчик для висловлення суб'єктивної думки:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Завдяки високій частоті оновлень (останні дані отримано 08 червня, 2026), канал підтримує актуальність та високий рівень охоплення публікацій. Аналітика показує, що аудиторія активно взаємодіє з контентом, що робить його важливою точкою впливу в категорії Технології та додатки.
readLock()
🔹 Для записи: writeLock()
Выбираем реализацию
Известно, что переменные читаются в 5 раз чаще, чем обновляются.
▫️ ReetrantLock и synchronized пускают только один поток в критическую секцию
▫️ ReadWriteLock запускает несколько потоков на чтение
Делаем прогноз, что ReadWriteLock разгромно победит остальные варианты.
Эксперимент
Измерим пропускную способность каждой реализации. Попробуем разную нагрузку и соотношение чтения-записи. Результаты сведём в график.
По горизонтали отметим разные кейсы. 5-1 означает, что в один момент 5 потоков читают значения, и 1 поток обновляет переменную.
По вертикали - пропускная способность. Чем выше график, тем лучше
Сам график - внизу поста⬇️
Результаты
Шок-контент! synchronized и ReetrantLock работают в 2-3 раза лучше, чем ReadWriteLock. Только в ситуации 10-1 его скорость немного приближается к конкурентам.
Почему ReadWriteLock проиграл?
Если кратко - сложная и неоптимальная реализация. ReetrantLock и synchronized используют простые конструкции и выигрывают у специализированного ReadWriteLock.
Логичен такой результат? Нет
Очевиден из чтения документации? Нет
Подобных ситуаций в JDK не так много. Большинство классов хорошо спроектированы, и при правильном использовании работают отлично.
————————
На курс многопоточки осталось 3 места. Старт уже в следующий понедельник.Point 2 поля: Х и Y. Нужно реализовать два метода: update(x,y) и getX().
Класс должен быть потокобезопасным. Планируется, что читаться поля будут в 5 раз чаще, чем обновляться.
Есть три реализации - на основе synchronized, ReentrantLock и ReadWriteLock. Какую выберете?do, do while, for в двух вариантах, Stream API. Выбирай, что удобно или привычно.
С многопоточкой такое не пройдёт. Здесь выбор инструмента влияет и на красоту кода, и на системные метрики. Расход памяти, задержки, скорость обработки и так далее.
Давайте разберём ситуацию в вопросе перед постом. Как выглядела картина ДО рефакторинга:
🔹 Сервис А в потоке X шлёт HTTP запрос в сервис Б.
🔹 Сервис Б принимает запрос, что-то считает и возвращает результат.
🔹 Пока Б развлекается, поток Х в сервисе А терпеливо ждёт.
Сколько (допустим) выполняется запрос /stat:
➕ Логика в сервисе А - 100мс
➕ Ожидание ответа сервиса Б - 500мс
➕ Обработка ответа - 100мс
Итого: 700мс
В java 11 добавилась опция асинхронных HTTP запросов. Что стало с системой ПОСЛЕ рефакторинга:
🔹 Сервис А шлёт асинхронный HTTP запрос в сервис Б.
🔹 Сервис Б работает над запросом.
🔹 Поток в сервисе А в это время делает другие задачи.
🔹 Cервис Б заканчивает работу.
🔹 На сервисе А вызывается коллбэк, который обрабатывает ответ сервиса Б.
Что получаем:
➕ Логика в сервисе А - 100мс
➕ Ожидание ответа от сервиса Б - 500мс
➕ Обработка ответа - 100мс
Итого: 700мс
Запрос /stat не стал быстрее.
Что изменилось?
Раньше поток сервиса А ждал ответ от Б и простаивал. Во время асинхронного запроса поток возвращается в планировщик и решает другие задачи.
Время выполнения запроса /stat не меняется, но общее количество работы, которое выполняет сервис А, увеличивается. Пропускная способность растёт: сервис обрабатывает больше запросов в секунду, чем раньше.
При небольшой нагрузке у сервиса может снизиться расход CPU. Но за такое редко выдают премии🙂
Так что правильный ответ на вопрос перед постом: увеличилась пропускная способность сервиса А.
Выводы
Иногда эффект многопоточных улучшений виден только под нагрузкой:
🔸 При работе с одним запросом разница может быть незаметна
🔸 Важно следить не только за кодом, но и за метрикамиGET /stat, который считает статистику. Для подсчёта используется внутренняя информация сервиса А и делается HTTP запрос GET /data в сервис Б.
Проект перевели на Java 11, и для доступа к сервису Б теперь используется асинхронный HTTP клиент. Стало значительно лучше, и менеджер проекта раздал всем премии.
Вопрос: Какая метрика улучшилась?Phaser хорошо виден разрыв между теорией и практикой. В этом посте расскажу, почему.
Паттерн Барьер помогает координировать потоки. Он блокирует один или несколько потоков, пока не наступит какое-то событие. JDK предлагает три реализации:
1️⃣ CountDownLatch
2️⃣ CyclicBarrier
3️⃣ Phaser
Последний - самый продвинутый:
🔸 Несколько сценариев работы
🔸 Методы мониторинга
🔸 Можно строить иерархичные структуры из нескольких Phaser
🔸 Иногда работает быстрее, чем CountDownLatch и CyclicBarrier
🔸 Обработка исключений
Класс Phaser часто встречается на воркшопах и advanced java курсах. Можно долго рассказывать про методы, рисовать схемы и многопоточно перемножать двумерные массивы.
Часто автор статьи или доклада держит фокус на инструменте:
Рассказываю про Phaser → Подбираю пример
На практике последовательность другая:
Вижу проблему → Ищу варианты → Выбираю подходящий
В такой цепочке у Phaser нет шансов. За пределами конференций и статей этот класс не используется.
Зачем тогда о нём говорить?
Чётко понимать, почему что-то НЕ работает, так же полезно, как и знать лучшие практики. Причиной может быть:
🔹 Плохой дизайн и неудобные методы
🔹 Неудачная реализация и проблемы производительности
🔹 Более подходящие инструменты
Чем плох Phaser?
Все реализации паттерна Барьер блокируют потоки, поэтому редко используются в нагруженных системах. Есть всего пара ситуаций, когда барьер - лучшее решение, но для их реализации достаточно CountDownLatch или CyclicBarrier. Phaser неплохо спроектирован, но слишком оторван от практических задач, это наглядный пример over engineering.message OrderRequest {
required int64 user_id = 1;
optional string address= 2;
repeated int64 item_id = 3;
}
Отправитель создаёт массив байтов опираясь на эту схему. Получатель считает данные по той же схеме.
✅ Короткие сообщения. Вместо имён полей используются порядковые номера(protobuf, Thrift), либо данные просто идут подряд(Avro, Parquet).
Schema Registry
И для текстовых, и для бинарных форматов остаётся проблема прямой и обратной совместимости. Менять схему можно, но в ограниченных пределах. Если формат данных меняется часто, то поможет паттерн Schema Registry.
Это отдельный компонент, который хранит все версии схем данных и сопутствующую информацию:
🔹 ID схемы: id = 15
🔹 Название: subject = "orderRequest"
🔹 Версия: version = 3
🔹 Сама схема: schema = …
Отправитель формирует сообщение и передаёт его вместе с ID схемы. Получатель берёт схему из Schema registry и читает данные. 100% совместимости это не гарантирует, но заметно упрощает работу.
Резюме:
Сериализация в java была отличным решением в своё время. Я не стала подробно описывать методы и лучшие практики Serializable/Externalizable, т.к такая сериализация осталась только в дремучих легаси проектах. Даже на собеседованиях её редко спрашивают.
Сейчас чаще используются форматы, не привязанные к конкретному языку и платформе. Но проблемы совместимости не исчезают:
🔸 Backward compatibility: чтение старых данных на новых серверах
🔸 Forward compatibility: чтение новых данных на старых серверах
Эти проблемы решаются двумя способами:
🔹 Адаптировать лучшие практики из Serializable. Подход рабочий, но набор доступных изменений сильно ограничен.
🔹 Использовать схемы данных. Они доступны для JSON, XML, SOAP, protobuf, Avro и т.д. Для упрощения работы со схемами поможет паттерн Schema Registry.private static final long serialVersionUID = 27507467L;В этом посте разберёмся, зачем это нужно, когда прописывать serialVersionUID и когда менять. В конце поговорим про недостатки сериализации в java. Итак, в процессе сериализации 2 участника: отправитель и получатель. У каждого из них есть код класса Х. Отправитель сериализует экземпляр Х и отправляет по сети. Из прошлого поста вы знаете, что в этом массиве байтов есть serialVersionUID. Получатель читает имя класса и первым делом сравнивает serialVersionUID из сообщения с serialVersionUID своего класса. ▫️ Если совпадают - начинается десериализация ▫️ Если нет - выбрасывается
InvalidClassException
Когда serialVersionUID не указан в классе явно, JVM вычисляет его в рантайме на основе имени класса, интерфейсов, полей и методов. Добавили новый метод - serialVersionUID изменился. Поэтому рекомендуется зафиксировать serialVersionUID, даже если поля класса не меняются.
Другой вариант - когда класс эволюционирует и передаёт другой набор данных. Сервисы не всегда обновляются одновременно, поэтому в переходный период возникают две проблемы:
🔸 Как новому коду читать данные, созданные старым кодом? (backward compatibility)
🔸 Как старому коду читать данные, созданные новым кодом? (forward compatibility)
Приходится мириться с наличием старых версий и писать код соответственно:
1️⃣ Задать в классе serialVersionUID. Никогда не менять
2️⃣ Добавить методы readObject и writeObject и прописать порядок записи и чтения полей
3️⃣ Писать тесты на совместимость версий
Набор изменений при этом весьма ограничен:
✅ Можно добавлять новые поля в конец байтового стрима
✅ Можно менять видимость полей и методов
❌ Нельзя удалять поля
❌ Нельзя менять тип полей
Пара "имя класса-serialVersionUID" работает как фильтр - можно десериализовать набор байтов или нет. Когда serialVersionUID не задан в классе, он генерируется JVM. Для прямой и обратной совместимости serialVersionUID может быть любым, но постоянным. Методы writeObject и readObject задают чёткий порядок чтения/записи, но сильно разгуляться не получится.
Уже отсюда понятно, что на практике с сериализацией море проблем:
▪️ Разработка усложняется: всегда нужно иметь в виду forward/backward совместимость, набор доступных изменений сильно ограничен.
▪️ Нарушается инкапсуляция, так как private поля передаются по сети.
▪️ Ограниченные сценарии использования. Получатель и отправитель должны быть на java.
▪️ Небезопасно. Десериализация - сладкий пирожок для разных типов атак. В 2016 их было так много, что тот год на конференциях называли Java deserialization apocalypse year. В 2021 году уязвимости на основе сериализации встречаются даже в Intellij IDEA и Kubernetes.
Что с этим делать и как сериализация выглядит на практике - поговорим в третьей части.class UserRequest implements SerializableУ него нет обязательных методов, это интерфейс-маркер. Интерфейс Externalizable даёт полный контроль над итоговым набором байтов. Хотите записать java объект в PDF или зашифровать данные - реализуйте методы Externalizable. Как происходит сериализация Serializable классов: 1️⃣ Проверка полей static и transient поля не участвуют в сериализации. Остальные поля должны быть либо Serializable, либо примитивами. Иначе разработчик получит NotSerializableException. 2️⃣ Объект превращается в байты Для передачи данных обычно используется ObjectOutputStream, но часто он скрыт за фреймворком или библиотекой. Что туда пишет JVM: 🔸 Поля-заголовки 🔸 Информация о классе: ▪️ Имя класса ▪️ serialVersionUID ▪️ Количество полей ▪️ Информация по каждому полю: ▫️ Тип (имя класса или примитив) ▫️ Длина ▫️ Имя переменной 🔸 Информация про Serializable родительские классы в таком же формате 🔸 Значения переменных Serializable родительских классов 🔸 Значения переменных текущего класса. Если переменная - не примитив, то схема повторяется - записывается информация про класс и значения полей. В примере перед постом класс Parent не реализует Serializable, поэтому parentValue не записывается в итоговый стрим, только childValue. 3️⃣ Набор байтов готов, можно отправлять. Десериализация по шагам Посмотрим на примере класса Parent и Child из примера выше. 1️⃣ Читаем из полученных байтов информацию о классе и о всех ближайших Serializable родителях. 2️⃣ Ищем ближайший НЕ Serializable родитель. В примере это класс Parent 3️⃣ Вызываем у класса Parent конструктор без параметров. Тут проставляется parentValue = 2 4️⃣ Получаем экземпляр. Конструктор Child не используется, остальные поля проставляются внутренними механизмами JVM. 5️⃣ Чтение полей из потока байтов. В нашем примере передано только childValue. Записываем: childValue = 50; Итого: в консоль выведется 2 и 50. Хотя изначально мы создавали объект с parentValue = 4, это поле не передаётся при сериализации, поэтому используется значение из конструктора Parent(). Как исправить ситуацию? Есть два варианта: 💊 Добавить классу Parent интерфейс Serializable 💊 Переопределить в классе Child методы writeObject и readObject. Они не определены в Serializable, но JVM найдёт их в процессе сериализации. В writeObject задаётся, какие поля и в каком порядке запишутся в итоговый объект:
private void writeObject(…out) {
out.writeInt(parentValue);
out.writeInt(childValue);
}
В readObject указывается, какие поля и в каком порядке читать из байтового стрима:
private void readObject(…in){
int parentValue=in.readInt();
setParentValue(parentValue);
this.childValue=in.readInt();
}
В любом из вариантов десериализованный объект напечатает 4 и 50.
В следующем посте поговорим, зачем в сообщении нужен serialVersionUID, когда его задавать напрямую и менять, а также про недостатки Java сериализации.
Вже доступно! Дослідження Telegram за 2025 — головні інсайти року 
