Многие знают, что 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
🔥21👍7❤1👏1
В многопоточном коде даже простая операция count++ не является атомарной. Она распадается на три шага:
1. Прочитать значение переменной.
2. Увеличить его.
3. Записать обратно.
Если два потока выполнят это одновременно, получится гонка данных (race condition). Итоговое значение будет меньше ожидаемого.
🔹 Как решает проблему AtomicInteger
AtomicInteger — это класс из пакета java.util.concurrent.atomic, который предоставляет атомарные (неделимые) операции над целыми числами.
Под капотом он использует механизм CAS (Compare-And-Swap), который поддерживается на уровне процессора.
Принцип работы:
— Читаем текущее значение.
— Проверяем, не изменилось ли оно за это время.
— Если совпадает, записываем новое.
— Если нет, повторяем попытку (spin loop).
🔹 Пример использования
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // атомарное ++
}
public int get() {
return count.get();
}
}
Здесь incrementAndGet() гарантирует, что два потока не «перетрут» друг друга, а каждый инкремент будет учтён.
🔹 Основные методы
▪️ get() — получить текущее значение.
▪️ set(int newValue) — установить новое значение.
▪️ incrementAndGet() / getAndIncrement() — инкремент.
▪️ decrementAndGet() — декремент.
▪️ addAndGet(int delta) — прибавить значение.
▪️ compareAndSet(int expect, int update) — вручную применить CAS.
🔹 Подводные камни
— Спин-блокировки
CAS может крутиться в цикле, пока не удастся обновить значение. При высокой конкуренции это нагружает процессор.
— ABA-проблема
Значение могло измениться на «A → B → A». Для простых счётчиков это не критично, но в сложных структурах данных используют AtomicStampedReference.
— Ограниченность
AtomicInteger работает только с int. Для более сложных случаев есть AtomicLong, AtomicReference, LongAdder (оптимизирован для высокой конкуренции).
🔹 Когда использовать
— Для простых счётчиков, метрик.
— В неблокирующих алгоритмах (lock-free).
— В высоконагруженных сценариях, где synchronized слишком дорог.
— Для сложных бизнес-операций над несколькими переменными (лучше использовать мьютексы или транзакции).
— При очень высокой конкуренции, может быть лучше взять LongAdder.
🔹 Итог
AtomicInteger — это лёгкий способ избавиться от гонок при работе с числами в многопоточности.
Это не магия, а всего лишь тонкая обёртка над CAS, встроенным в процессоры. Понимание этого механизма помогает писать по-настоящему безопасный и быстрый многопоточный код.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤7🔥2
👀 Внутреннее устройство LinkedList
LinkedList — это классическая реализация двусвязного списка. На поверхности он выглядит как обычная коллекция, реализующая интерфейсы List, Deque и Queue. Но под капотом это структура узлов (Node), которые связаны друг с другом через ссылки на предыдущий и следующий элемент.
📦 Базовая структура
Каждый элемент списка хранится в отдельном объекте Node<E>, который содержит:
▪️ E item — сам элемент
▪️ Node<E> next — ссылка на следующий узел
▪️ Node<E> prev — ссылка на предыдущий узел
У LinkedList есть два поля:
▪️ first — голова списка
▪️ last — хвост списка
Это позволяет быстро добавлять элементы в начало и конец.
⚡️ Добавление и удаление
— Добавление в начало (addFirst) или конец (addLast) → O(1): меняем ссылки у пары узлов.
— Удаление головы или хвоста также → O(1).
— Вставка или удаление в середине требует сначала дойти до нужного узла → O(n).
🌊 Поиск элемента
— По индексу: список не хранит массив, значит придётся идти по ссылкам.
— Оптимизация: если индекс ближе к голове, обход идёт с first, если к хвосту — с last.
Сложность в среднем — O(n/2), то есть линейная.
📊 Производительность
— Доступ по индексу → O(n).
— Добавление/удаление в начало или конец → O(1).
— Вставка/удаление в середину → O(n).
— Итерация по списку → O(n), но эффективно, так как используется последовательный проход по ссылкам.
⚖️ Важные нюансы
— В отличие от ArrayList, в LinkedList нет операций с массивами и «ресайзинга».
— Но расходует больше памяти: каждый узел хранит не только элемент, но и две ссылки (prev/next).
— Итераторы fail-fast: изменение списка во время обхода бросает ConcurrentModificationException.
🔄 Итераторы и Deque
LinkedList реализует Deque, что делает его удобным для очередей и стеков. Offer, poll, peek работают за O(1). Push/pop превращают список в стек.
🧮 Когда использовать
На практике ArrayList почти всегда быстрее по времени и эффективнее по памяти.
LinkedList может быть полезен только в редких случаях, когда нужны очень частые вставки/удаления в середину коллекции (без итерации по коллекции) и не важен доступ по индексу. В остальных случаях выбирайте ArrayList.
🔗 Документация: OpenJDK — LinkedList source | Официальная JavaDoc (Java 17)
Ставьте 🔥, если хотите такой же пост по другим коллекциям, например CopyOnWriteArrayList.
🐸 Библиотека джависта
#CoreJava
LinkedList — это классическая реализация двусвязного списка. На поверхности он выглядит как обычная коллекция, реализующая интерфейсы List, Deque и Queue. Но под капотом это структура узлов (Node), которые связаны друг с другом через ссылки на предыдущий и следующий элемент.
📦 Базовая структура
Каждый элемент списка хранится в отдельном объекте Node<E>, который содержит:
▪️ E item — сам элемент
▪️ Node<E> next — ссылка на следующий узел
▪️ Node<E> prev — ссылка на предыдущий узел
У LinkedList есть два поля:
▪️ first — голова списка
▪️ last — хвост списка
Это позволяет быстро добавлять элементы в начало и конец.
⚡️ Добавление и удаление
— Добавление в начало (addFirst) или конец (addLast) → O(1): меняем ссылки у пары узлов.
— Удаление головы или хвоста также → O(1).
— Вставка или удаление в середине требует сначала дойти до нужного узла → O(n).
🌊 Поиск элемента
— По индексу: список не хранит массив, значит придётся идти по ссылкам.
— Оптимизация: если индекс ближе к голове, обход идёт с first, если к хвосту — с last.
Сложность в среднем — O(n/2), то есть линейная.
📊 Производительность
— Доступ по индексу → O(n).
— Добавление/удаление в начало или конец → O(1).
— Вставка/удаление в середину → O(n).
— Итерация по списку → O(n), но эффективно, так как используется последовательный проход по ссылкам.
⚖️ Важные нюансы
— В отличие от ArrayList, в LinkedList нет операций с массивами и «ресайзинга».
— Но расходует больше памяти: каждый узел хранит не только элемент, но и две ссылки (prev/next).
— Итераторы fail-fast: изменение списка во время обхода бросает ConcurrentModificationException.
🔄 Итераторы и Deque
LinkedList реализует Deque, что делает его удобным для очередей и стеков. Offer, poll, peek работают за O(1). Push/pop превращают список в стек.
🧮 Когда использовать
На практике ArrayList почти всегда быстрее по времени и эффективнее по памяти.
LinkedList может быть полезен только в редких случаях, когда нужны очень частые вставки/удаления в середину коллекции (без итерации по коллекции) и не важен доступ по индексу. В остальных случаях выбирайте ArrayList.
Ставьте 🔥, если хотите такой же пост по другим коллекциям, например CopyOnWriteArrayList.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥19❤2👍2😁2
🧩 final vs finally vs finalize()
Казалось бы, три похожих ключевых слова → final, finally, finalize(). А смысл принципиально разный. Давайте разберёмся.
🔹 final
Модификатор, который делает сущность неизменяемой:
▪️ final переменная → нельзя переприсвоить.
▪️ final метод → нельзя переопределить.
▪️ final класс → нельзя наследовать.
Используется для обеспечения immutability и контрактов в коде.
🔹 finally
Блок в try-catch, который выполняется всегда (даже если выброшено исключение).
Гарантирует освобождение ресурсов:
🔹 finalize()
Метод класса Object, вызываемый GC перед удалением объекта. Используется редко, считается устаревшим (deprecated с Java 9, удалён в Java 18).
Минус: непредсказуемое время вызова.
Современная альтернатива: try-with-resources или явная очистка.
💡 Вывод
final → контроль изменяемости.
finally → контроль завершения.
finalize() → контроль очистки (но не используйте).
🐸 Библиотека джависта
#CoreJava
Казалось бы, три похожих ключевых слова → final, finally, finalize(). А смысл принципиально разный. Давайте разберёмся.
🔹 final
Модификатор, который делает сущность неизменяемой:
▪️ final переменная → нельзя переприсвоить.
▪️ final метод → нельзя переопределить.
▪️ final класс → нельзя наследовать.
Используется для обеспечения immutability и контрактов в коде.
🔹 finally
Блок в try-catch, который выполняется всегда (даже если выброшено исключение).
Гарантирует освобождение ресурсов:
try {
FileReader reader = new FileReader("data.txt");
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("Закрываем ресурсы");
}
🔹 finalize()
Метод класса Object, вызываемый GC перед удалением объекта. Используется редко, считается устаревшим (deprecated с Java 9, удалён в Java 18).
Минус: непредсказуемое время вызова.
Современная альтернатива: try-with-resources или явная очистка.
final → контроль изменяемости.
finally → контроль завершения.
finalize() → контроль очистки (но не используйте).
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥1👏1
Spring Boot — это не «новый фреймворк», а надстройка над Spring, которая убирает рутину и ускоряет разработку.
В обычном Spring нужно было вручную конфигурировать всё: от DataSource до DispatcherServlet. В Boot это делается автоматически через автоконфигурацию.
🔹 Как работает автоконфигурация
Spring Boot сканирует зависимости и classpath, а затем подключает нужные бины:
— Если у вас есть spring-boot-starter-data-jpa, Boot автоматически создаст EntityManagerFactory, DataSource, транзакционный менеджер.
— Если добавлен spring-boot-starter-web, он поднимет встроенный Tomcat/Jetty и зарегистрирует контроллеры. И так далее.
Магия кроется в аннотации @EnableAutoConfiguration (включается через @SpringBootApplication). Она загружает META-INF/spring.factories → список классов-конфигов → каждый проверяет условия @ConditionalOnClass, @ConditionalOnMissingBean и решает: активироваться или нет.
Потому что в Boot сотни готовых конфигураций «на все случаи жизни».
Фактически, это огромная библиотека «если увидишь X — настрой Y».
— Черный ящик
Легко забыть, что именно сконфигурировал Boot. Иногда приходится «копать» в автоконфигурацию, чтобы понять, какой бин реально используется.
— Избыточные зависимости
Подключив Starter, можно случайно притащить половину экосистемы Spring. Это увеличивает время старта и усложняет дебаг.
— Конфликт настроек
Собственная конфигурация может пересечься с автоконфигурацией.
⚡️ Хорошая практика
— Не доверяйте «чёрному ящику»: при старте приложения смотрите Spring Boot Actuator и логи автоконфигурации.
— Знайте про
--debug
при старте: он показывает, какие автоконфигурации включены или отключены.— В продакшене лучше контролировать, какие именно стартеры вы тянете. Иногда spring-boot-starter-web приносит в проект в три раза больше, чем реально нужно.
🎯 Итог
Spring Boot — это ускоритель, но не магия. Его сила в автоконфигурациях, а слабость в том, что легко потерять контроль.
Понимание того, как работает @EnableAutoConfiguration и условия @Conditional, отличает разработчика, который «просто пишет на Boot», от того, кто реально управляет приложением.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤2🔥1😁1
🔥 Задача на алгоритмы: оптимизация расписания задач
Иногда даже повседневные задачи превращаются в отличный повод вспомнить алгоритмы и потренировать мозг. Особенно если представить, что это часть сервиса планирования нагрузок или распределения вычислений в распределённой системе.
🔹 Условие
— У вас есть N задач, каждая с временем выполнения t[i].
— Есть K воркеров (потоков, серверов), которые могут выполнять задачи параллельно, но каждый воркер может брать только одну задачу за раз.
💡 Пример:
🔹 Уточнения
— N может достигать 10⁴
— K до 100
— Допустимо небольшое отклонение от оптимума (например, ≤5%)
— Требуется O(N log N) или лучше
— Можно предусмотреть балансировку “на лету” при поступлении новых задач
🔹 Вопрос
Какой алгоритм примените для минимизации времени выполнения?
Подумайте о вариантах:
— жадный
— динамическое программирование
— приближённые решения (если N велико)
🔹 Подсказки
—Это классическая NP-трудная задача разбиения множества (Partition Problem)
—На практике часто решается жадным алгоритмом LPT (Longest Processing Time first)
👇🏻 Скелет решения предложили в комментах.
💬 Делитесь своими решениями: какой алгоритм выбрали бы для продакшена, а какой — для интервью?
🐸 Библиотека джависта
#CoreJava
Иногда даже повседневные задачи превращаются в отличный повод вспомнить алгоритмы и потренировать мозг. Особенно если представить, что это часть сервиса планирования нагрузок или распределения вычислений в распределённой системе.
🔹 Условие
— У вас есть N задач, каждая с временем выполнения t[i].
— Есть K воркеров (потоков, серверов), которые могут выполнять задачи параллельно, но каждый воркер может брать только одну задачу за раз.
Нужно распределить задачи между воркерами так, чтобы время завершения всех задач было минимальным.
t = [3, 7, 2, 5, 4]
K = 2
— если раздать задачи просто по очереди, один воркер закончит через 14 секунд, другой — через 7.
— а если распределить умнее (например, [7,3] и [5,4,2]), итоговое время — 10 секунд.
🔹 Уточнения
— N может достигать 10⁴
— K до 100
— Допустимо небольшое отклонение от оптимума (например, ≤5%)
— Требуется O(N log N) или лучше
— Можно предусмотреть балансировку “на лету” при поступлении новых задач
🔹 Вопрос
Какой алгоритм примените для минимизации времени выполнения?
Подумайте о вариантах:
— жадный
— динамическое программирование
— приближённые решения (если N велико)
🔹 Подсказки
—
—
👇🏻 Скелет решения предложили в комментах.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4👏2🔥1