Многие знают, что volatile «гарантирует видимость между потоками». Давайте разберёмся, что именно делает это ключевое слово на уровне Java Memory Model (JMM).
В многопроцессорной архитектуре потоки могут обращаться к своим копиям переменных через L1/L2 кэши, а не к основной памяти. Компилятор и процессор активно реорганизуют инструкции (out-of-order, speculative execution), что при отсутствии механизмов синхронизации ведёт к неожиданному поведению в многопоточности.
Если переменная не volatile, один поток может работать со старым значением из кеша и никогда не увидеть обновление другим потоком.
Пример:
class FlagExample {
boolean flag = false;
void thread1() {
while (!flag) {
// бесконечный цикл
}
}
void thread2() {
flag = true;
}
}
JVM и JIT могут оптимизировать while(!flag) так, что значение flag будет читаться один раз и кешироваться локально — и первый поток не выйдет из цикла.
🔹 Гарантирует видимость — каждое чтение/запись идёт из основной памяти, а не из локальных кешей
🔹 Гарантирует порядок операций — volatile запрещает компилятору и процессору менять порядок операций до и после чтения/записи
🔹 Гарантирует правило happens-before — запись в переменную будет видна всем потокам, которые читают её после
Пример:
volatile int counter = 0;
void inc() {
if (counter < 10) {
counter++; // всё ещё не безопасно
}
}
▪️ Не обеспечивает атомарность
counter++ разваливается на чтение → инкремент → запись. Если два потока сделают это одновременно, одно из увеличений потеряется.
Для атомарных операций используйте атомики или синхронизацию.
▪️ Не заменяет synchronized
volatile гарантирует видимость, но не защищает этот блок от одновременного выполнения разными потоками. Для таких случаев используйте synchronized или Lock.
Пример double-checked locking:
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Без volatile JVM могла бы отдать ссылку на не до конца сконструированный объект.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥3👏2
⚙️ Интеграция системы логирования с Logback в Spring Boot
Ищете, как настроить логирование в приложении на Spring Boot? Используйте Logback для эффективного логирования и управления уровнями логов. Используйте AI для ускорения процесса.
📝 Промпт:
💡 Расширения:
— Добавьте
— Добавьте
— Добавьте
🐸 Библиотека джависта
#CoreJava
Ищете, как настроить логирование в приложении на Spring Boot? Используйте Logback для эффективного логирования и управления уровнями логов. Используйте AI для ускорения процесса.
📝 Промпт:
Generate a logging system integration for a Spring Boot 3 application using Logback.
— Set up logback-spring.xml for flexible configuration of logging levels and appenders.
— Define rolling file appender to archive old log files based on size or date.
— Configure log format with PatternLayoutEncoder for structured and readable logs.
— Implement different logging levels (INFO, WARN, ERROR) for different components of the application.
— Integrate MDC (Mapped Diagnostic Context) for tracking user sessions or specific requests.
— Set up asynchronous logging with AsyncAppender to improve performance in high-traffic applications.
— Enable console logging for development and file logging for production environments.
— Добавьте
Set up rolling file appender for log rotation based on size or date
для архивирования старых логов.— Добавьте
Implement asynchronous logging with AsyncAppender
для улучшения производительности в высоконагруженных приложениях.— Добавьте
Integrate custom appenders for external monitoring systems like Elasticsearch or Splunk
для интеграции с внешними системами мониторинга.#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤2🔥1
Garbage Collector (GC) в Java — это механизм автоматического управления памятью, который отвечает за очистку памяти от объектов, которые больше не используются в программе. Вместо того, чтобы разработчик вручную освобождал память, как в некоторых других языках программирования, Java использует сборщик мусора, который делает это автоматически.
Когда вы создаёте объект в Java, он занимает место в куче (heap) — области памяти, предназначенной для динамического распределения. Однако по мере работы программы некоторые объекты становятся ненужными, и их можно удалить, чтобы освободить память для других задач. Это и есть основная задача GC — найти объекты, которые больше не используются, и освободить память.
1. Маркировка. На первом этапе система анализирует объекты в куче и помечает те, на которые существуют ссылки, то есть которые всё ещё могут быть использованы в программе. Эти объекты называют живыми.
2. Сборка мусора. После маркировки GC удаляет объекты, которые не были помечены как «живые». Эти объекты больше не используются в программе и могут быть безопасно удалены, а занимаемое ими пространство освобождается.
3. Компактизация (Compaction). Иногда после удаления объектов в куче остаются фрагменты пустой памяти. В этом случае GC может перемещать объекты, чтобы устранить фрагментацию и сделать память более сплошной. Это улучшает использование доступных ресурсов.
Java предлагает несколько типов сборщиков мусора, каждый из которых имеет свои особенности и подходит для разных сценариев:
— Serial GC: простой сборщик, использующий один поток для работы с памятью. Это может быть полезно в простых приложениях, но вызывает большие паузы в работе программы, что не подходит для сложных многозадачных приложений.
— Parallel GC: этот сборщик использует несколько потоков для работы, что ускоряет процесс очистки. Он подходит для многозадачных приложений и приложений с большими объемами данных, где важно минимизировать время пауз.
— CMS (Concurrent Mark-Sweep): сборщик мусора, который работает параллельно с основной программой, минимизируя паузы. Он использует несколько шагов для маркировки и уборки мусора, чтобы не блокировать выполнение приложения на долгое время.
— G1 (Garbage First): один из самых современных сборщиков мусора. Он фокусируется на минимизации времени пауз и дает разработчикам больше контроля над процессом. G1 отлично подходит для больших приложений с высоким уровнем взаимодействия.
Одним из главных аспектов работы GC является Stop-the-World пауза, когда приложение временно приостанавливается, чтобы сборщик мусора очистил память. Хотя паузы в большинстве случаев довольно короткие, они могут заметно повлиять на производительность, особенно в приложениях с высокими требованиями к времени отклика.
— Оптимизация размера кучи. Размер кучи можно настроить в зависимости от объема данных, с которым работает ваше приложение. Неправильно выбранный размер может привести к слишком частым или слишком редким сборкам мусора.
— Использование правильного сборщика. Выбор сборщика мусора зависит от особенностей вашего приложения. Например, для приложений с требованием низкой задержки лучше использовать G1 или CMS.
— Профилирование. Используйте инструменты профилирования, чтобы отслеживать, как работает GC в вашем приложении. Это поможет выявить проблемы и оптимизировать использование памяти.
— Когда ваше приложение работает с большим количеством объектов, и необходимо следить за производительностью.
— Если заметны задержки или паузы, вызванные работой GC, и нужно оптимизировать работу с памятью.
— В сложных многозадачных или распределённых приложениях, где важно, чтобы GC не блокировал выполнение других задач.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤1🔥1👏1
Часто выбор коллекции ограничивается только таблицей сложностей и на этом всё.
Но реальный кейс сложнее: средняя сложность ≠ реальная скорость в продакшне. JVM, кэш процессора, GC и паттерны доступа могут радикально поменять картину.
🔑 Главная мысль
Выбирайте коллекцию под сценарий использования, а не “по самой быстрой ячейке в таблице”.
ArrayList хранит элементы в массиве → локальность памяти + CPU кэш → итерации летят.
Вставка в середину за O(n), но при небольших списках разница с LinkedList исчезающе мала.
🔧 Паттерн использования:
— 90% чтение, редкие вставки → идеально.
— Если заранее известно примерное кол-во элементов → задайте initialCapacity, иначе ArrayList будет несколько раз пересоздавать массив (copy O(n) на каждом росте).
В бенчмарках JMH даже при вставке в середину ArrayList часто быстрее LinkedList просто потому, что LinkedList платит за “pointer chasing” (скачки по памяти, cache-miss).
Да, вставка/удаление в начало или конец за O(1).
Но get(i) = O(n), и каждый шаг = новый объект, новая ссылка → нагрузка на GC.
🔧 Паттерн использования:
— Когда нужна двусторонняя очередь с частыми удалениями/добавлениями в начало и конец.
— Во всех остальных случаях лучше ArrayDeque, он без лишних объектов и быстрее почти всегда.
LinkedList ест больше памяти: на каждый элемент два указателя + объект-узел.
HashMap даёт O(1) доступ при хорошем hashCode().
Но:
— Если хэши “плохие” → коллизии → O(log n)
— При достижении load factor 0.75 → resize → перераспределение всех бакетов (дорогая операция).
🔧 Паттерн использования:
— Когда нужен быстрый поиск по ключу без сохранения порядка или когда важно хранить уникальные элементы или строить словари/кэши по ключу.
— Если знаете примерное кол-во элементов → сразу задайте кол-во элементов в конструкторе new HashMap<>(N).
Начиная с Java 8 при коллизии, когда LinkedList становится длинным (по умолчанию ≥ 8 элементов) → список превращается в красно-чёрное дерево.
Дают O(log n) доступ и всегда хранят ключи отсортированными.
Но если сортировка нужна редко, дешевле собрать HashMap и вызвать sorted() на стриме.
🔧 Паттерн использования:
— Когда важно поддерживать сортировку на каждой операции (напр. Top-N задач в приоритетной очереди).
— Не храните mutable-ключи, т.к. можно “потерять” элемент при изменении поля, участвующего в compareTo.
TreeMap хранит узлы с балансировкой (красно-чёрное дерево) → накладные расходы на память + сравнения ключей.
LinkedHashMap поддерживает порядок вставки или порядок доступа (accessOrder=true).
Можно сделать LRU-кэш, переопределив removeEldestEntry.
🔧 Паттерн использования:
— Когда важен порядок, но сортировка не нужна.
— Когда нужно легко реализовать ограниченный кэш.
Каждый get() в режиме accessOrder вызывает перестановку в двусвязном списке → небольшие накладные расходы.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21❤9🔥4
💎 Как работает String.intern() под капотом
Многие знают, что строки в Java «живут в пуле». Но что реально происходит, когда мы вызываем intern()?
1️⃣ Что такое String Pool
Java хранит все строковые литералы в специальной области памяти — String Intern Pool. Зачем? Чтобы одинаковые строки не занимали память несколько раз.
Обе ссылки указывают на одну и ту же строку из пула.
2️⃣ Как работает intern()
Если вызвать s.intern():
— JVM проверит, есть ли такая строка в пуле.
— Если есть, вернёт ссылку на неё.
— Если нет, добавит текущую строку в пул и вернёт ссылку.
Пример:
3️⃣ Под капотом JVM
🔹 До Java 7 String Pool находился в PermGen → можно было легко словить OOM.
🔹 Начиная с Java 7 (HotSpot) пул перенесли в heap, что сильно упростило жизнь.
🔹 Реализация — ConcurrentHashMap внутри JVM, так что intern() потокобезопасен.
4️⃣ Где это полезно
🔹 Оптимизация памяти при большом количестве одинаковых строк (например, при парсинге XML/JSON).
🔹 Сравнение строк через == (только если они гарантированно interned).
🔹 При работе с ключами в больших мапах, чтобы уменьшить дубли.
5️⃣ Подводные камни
🔹 intern() не бесплатен — поиск в пуле и вставка стоят ресурсов.
🔹 Чрезмерное использование может привести к росту heap-а и GC-паузам.
🔹 Нельзя бездумно использовать вместо обычных строк → легко получить деградацию.
🔗 Документация: String.intern()
💬 Использовали когда-нибудь intern() в продакшене?
🐸 Библиотека джависта
#CoreJava
Многие знают, что строки в Java «живут в пуле». Но что реально происходит, когда мы вызываем intern()?
Java хранит все строковые литералы в специальной области памяти — String Intern Pool. Зачем? Чтобы одинаковые строки не занимали память несколько раз.
String a = "hello";
String b = "hello";
System.out.println(a == b); // true
Обе ссылки указывают на одну и ту же строку из пула.
Если вызвать s.intern():
— JVM проверит, есть ли такая строка в пуле.
— Если есть, вернёт ссылку на неё.
— Если нет, добавит текущую строку в пул и вернёт ссылку.
Пример:
String x = new String("world");
String y = x.intern();
String z = "world";
System.out.println(x == z); // false
System.out.println(y == z); // true
🔹 До Java 7 String Pool находился в PermGen → можно было легко словить OOM.
🔹 Начиная с Java 7 (HotSpot) пул перенесли в heap, что сильно упростило жизнь.
🔹 Реализация — ConcurrentHashMap внутри JVM, так что intern() потокобезопасен.
🔹 Оптимизация памяти при большом количестве одинаковых строк (например, при парсинге XML/JSON).
🔹 Сравнение строк через == (только если они гарантированно interned).
🔹 При работе с ключами в больших мапах, чтобы уменьшить дубли.
🔹 intern() не бесплатен — поиск в пуле и вставка стоят ресурсов.
🔹 Чрезмерное использование может привести к росту heap-а и GC-паузам.
🔹 Нельзя бездумно использовать вместо обычных строк → легко получить деградацию.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤3🤔2🔥1
Чтобы вам было проще ориентироваться в постах, мы разделили весь контент по 4 основным направлениям:
🔹 #CoreJava — фундаментальные знания: JVM, JDK, ООП, многопоточность, паттерны и базовые концепции. Всё, что помогает понимать Java глубже, а не просто «писать код, чтобы работало».
🔹 #Enterprise — прикладные инструменты и практика: Spring, Hibernate, Kafka, Docker, микросервисы. Всё, что встречается в работе разработчика каждый день.
🔹 #DevLife — сообщество и карьера: мемы, холивары, задачи с собесов, советы по развитию и личные рубрики. Всё, что создаёт атмосферу и объединяет нас как комьюнити.
🔹 #News — дайджесты, свежие анонсы, релизы и новости. А также реклама и инфоповоды, которые стоит знать.
👉 Используйте теги, чтобы быстро находить посты по интересующей теме.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥3👏1
ORM (Object-Relational Mapping) — это мост между объектами Java и реляционными базами данных. Вместо того, чтобы писать SQL-запросы руками, вы работаете с привычными объектами и методами.
🔹 Зачем нужен ORM
В Java идет работа с классами, полями, методами. В базе данных всё хранится в виде таблиц, строк и столбцов. ORM выступает «переводчиком»:
— Таблица ↔️ Класс
— Строка ↔️ Объект
— Столбец ↔️ Поле
ORM берёт на себя «грязную работу»:
1. При создании объекта в коде (new User("Alex")) и вызове save(), ORM формирует SQL-запрос
INSERT INTO users (name) VALUES ('Alex')
и отправляет его в базу.2. Когда вы хотите достать данные (userRepository.findById(1)), ORM делает
SELECT * FROM users WHERE id=1
, создаёт объект User и наполняет его полями из результата запроса.3. Если вы меняете поле (user.setName("Ivan")) и сохраняете, ORM сгенерирует
UPDATE users SET name='Ivan' WHERE id=1
.4. Если объект больше не нужен и вы вызываете delete(), ORM сформирует
DELETE FROM users WHERE id=1
.То есть вы оперируете объектами и методами, а ORM переводит ваши действия в SQL и обратно.
🔹 Плюсы ORM
— Меньше шаблонного кода. Не нужно постоянно писать INSERT, SELECT, UPDATE.
— Более читаемо. Идет работа с методами вроде userRepository.findByEmail(), а не с SQL запросами.
— Кросс-СУБД. ORM умеет подстраиваться под разные базы (PostgreSQL, MySQL, Oracle).
— Интеграция. Легко комбинируется со Spring и другими фреймворками.
🔹 Минусы ORM
— Иллюзия простоты. Кажется, что можно забыть про SQL, но «под капотом» всё равно генерируются запросы.
— Проблема N+1. Частая ошибка, когда ORM делает сотни мелких запросов вместо одного «жирного».
— Сложные кейсы. Для тяжёлой аналитики или оптимизации всё равно пишут чистый SQL или хранят процедуры.
— Производительность. ORM добавляет прослойку, которая может стать узким местом при больших нагрузках.
🔹 Типичные ловушки ORM
— Ленивая загрузка (Lazy Loading). Может неожиданно тянуть данные из БД в середине транзакции.
— Кэширование. ORM кэширует объекты, но если работать невнимательно, то легко получить «старые» данные.
— Транзакции. Многие забывают, что ORM не волшебная палочка: без грамотной работы с транзакциями данные могут остаться в полупрозрачном состоянии.
🔹 Основные инструменты в Java
— JPA (Java Persistence API). Стандарт, описывающий, как именно должны работать ORM-инструменты.
— Hibernate. Самый популярный провайдер JPA, фактический стандарт в экосистеме Java.
— EclipseLink, OpenJPA. Альтернативные реализации, реже используемые.
— Spring Data JPA. Надстройка над JPA, позволяющая писать репозитории и автогенерировать запросы из названий методов.
🔹 ORM или не ORM
— Приложение типовое (CRUD, REST API, веб-сервисы)
— Важна скорость разработки и поддерживаемость
— Команда не хочет тратить часы на ручные SQL-запросы
— Высоконагруженная система с миллионами записей
— Важен каждый миллисекундный отклик
— Есть сложная аналитика или отчёты (там SQL быстрее и прозрачнее)
ORM — это «помощник», а не замена знаний SQL. Он снимает рутину и ускоряет разработку, но требует грамотного использования. Понимать, что происходит «под капотом», — ключ к тому, чтобы не наступать на грабли.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤6🔥2
На выходных можно подумать о высоком и попрактиковать дизайн систем. Полезно для будущих собесов, а кому-то просто для тренировки. В реальной задаче функциональные и нефункциональные требования ещё нужно было бы уточнить, но в формате поста сразу добавили их.
🔹 Условие
Перед днём рождения коллеги выбирается ответственный, ему скидывают деньги, он что-то покупает, а потом надо решать, что делать со сдачей. Сервис должен:
— Позволять зарегистрированным пользователям скидываться в общую «копилку»
— Хранить средства до момента покупки
— Дать возможность выбрать товары из каталога маркетплейса
— После закрытия копилки — потратить средства на выбранное и вернуть остаток кэшбеком
🔹 Функциональные требования
— Создание и управление «копилкой»
— Пополнение копилки разными пользователями
— Привязка к каталогу и корзине маркетплейса
— Сценарий покупки и списания денег
— Возврат сдачи кэшбеком
— Уведомления участников о статусе
🔹 Нефункциональные требования
— До 200k DAU, всего ~1 млн пользователей
— Запуск MVP в течение квартала
— Масштабируемость под рост аудитории
— Интеграция с существующими сервисами: биллинг, пользователи, каталог, кэшбек, логистика
— SLA: быстрый отклик API (≤200 мс), отказоустойчивость, логирование
— Если нужно уточнить какие-то вопросы: можете писать в комментарии, расширим вводные
— Подумайте о жизненном цикле копилки:
— Разбейте на сущности:
— Продумайте API:
— Учтите рост нагрузки:
— Не забудьте про безопасность:
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👍2❤1
Используете ORM, но путаетесь в терминах: JPA, Hibernate, Spring Data JPA? Давайте разложим по полочкам.
🔹 JPA (Java Persistence API)
— Это спецификация, а не инструмент.
— Определяет, как описывать сущности (@Entity, @Id, @OneToMany и т.д.).
— Определяет стандартные операции: EntityManager.persist(), find(), remove().
— Не содержит «железа» — только контракт.
👉 JPA = «договор о том, как работать с ORM в Java».
🔹 Hibernate
— Это реализация JPA, самый популярный ORM-фреймворк.
— Реально выполняет работу: маппинг объектов ↔️ таблиц, генерация SQL.
— Добавляет много фич «поверх» JPA (например, кэширование второго уровня, собственные аннотации).
— Можно использовать напрямую, но чаще его подключают как провайдер JPA.
👉 Hibernate = «двигатель», который выполняет то, что описано в JPA.
🔹 Spring Data JPA
— Это надстройка над JPA (и Hibernate под капотом).
— Упрощает жизнь: вместо EntityManager вы пишете интерфейсы UserRepository, а Spring сам генерирует реализацию.
— Позволяет писать запросы прямо в именах методов (findByEmailAndStatus).
— Поддерживает @Query для кастомных запросов.
— Даёт дополнительные удобства: пэйджинг, сортировки, спецификации.
👉 Spring Data JPA = «фасад», который избавляет от рутины и скрывает низкоуровневые детали.
🔹 Когда что использовать
— Если нужен чистый стандарт → JPA + реализация (например, Hibernate).
— Если хотите гибкость и полный контроль → можно работать напрямую с Hibernate.
— Если важна скорость разработки и минимум кода → Spring Data JPA.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11❤2🔥1
HashMap — один из базовых и в то же время самых хитро устроенных контейнеров в JDK. На поверхности простая структура «ключ–значение», но под капотом она сочетает массивы, списки и даже деревья, чтобы оставаться быстрой в разных сценариях нагрузки.
📦 Базовая структура
HashMap хранит данные в массиве бакетов
(Node<K,V>[] table)
. Каждый бакет — это «корзина» для элементов, чей hashCode после хеширования и применения & (n-1)
(где n — длина массива) указывает на конкретный индекс.🔑 Хэш и распределение
1. Вызов hashCode() у ключа.
2. Дополнительное хеширование (spread), чтобы снизить коллизии из-за плохой реализации hashCode.
3. Индекс бакета =
hash & (table.length - 1)
.🌊 Коллизии
Если несколько ключей попали в один бакет:
— до Java 8 это всегда был связанный список (linked list),
— начиная с Java 8: при росте числа элементов в бакете больше 8 и достаточном размере таблицы он превращается в сбалансированное красно-чёрное дерево. Это резко ускоряет поиск в «плохих» случаях (с O(n) до O(log n)).
⚡️ Ресайзинг
Когда количество элементов превышает
capacity * loadFactor
(по умолчанию 0.75), создаётся новый массив в 2 раза больше, все элементы перехешируются и раскладываются по новым бакетам. Это дорогостоящая операция, но благодаря амортизации остаётся приемлемой.📊 Производительность
— Поиск/вставка/удаление в среднем: O(1).
— В худшем случае (плохой hashCode + коллизии): O(log n) благодаря деревьям.
⚖️ Важные нюансы
— Ключи неупорядочены. Для упорядоченности есть LinkedHashMap.
— HashMap не потокобезопасен. Для многопоточной среды нужен ConcurrentHashMap или синхронизация.
— Хорошо реализованный hashCode и equals критичны, иначе получите «забитые» бакеты и деградацию.
🧮 loadFactor и capacity
— Capacity — размер массива бакетов. По умолчанию 16.
— LoadFactor — коэффициент заполнения. По умолчанию 0.75.
Почему именно 0.75? Это компромисс: выше → меньше памяти, но больше коллизий; ниже → быстрее доступ, но больше памяти уходит впустую. Capacity всегда степень двойки, чтобы можно было вычислять индекс через
hash & (n-1)
вместо затратного %.🔄 Итераторы и fail-fast
Если во время обхода карта меняется (кроме
iterator.remove()
), бросается ConcurrentModificationException. Под капотом это работает через счётчик модификаций (modCount), который проверяется в каждом next().🌳 Деревья в деталях
Коллизии превращаются в красно-чёрное дерево, если размер списка в бакете > 8 и общее количество бакетов ≥ 64. Обратно в список (untreeify) при падении количества элементов < 6. Это сделано, чтобы не тратить память и CPU на лишнюю балансировку при малых размерах.
Ставьте 🔥, если хотите такой же пост по другим коллекциям.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥22👍7❤1👏1