Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Show more📈 Analytical overview of Telegram channel Java: fill the gaps
Channel Java: fill the gaps (@java_fillthegaps) in the Russian language segment is an active participant. Currently, the community unites 12 549 subscribers, ranking 10 121 in the Technologies & Applications category and 52 862 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 12 549 subscribers.
According to the latest data from 07 June, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -46 over the last 30 days and by 0 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 34.72%. Within the first 24 hours after publication, content typically collects N/A% reactions from the total number of subscribers.
- Post reach: On average, each post receives 0 views. Within the first day, a publication typically gains 0 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 0.
- Thematic interests: Content is focused on key topics such as redis, hashmap, linkedhashmap, индекс, фича.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Thanks to the high frequency of updates (latest data received on 08 June, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
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 сериализации.
Available now! Telegram Research 2025 — the year's key insights 
