Давайте разберем три принципиально разных подхода к многопоточности и посмотрим, к чему они приводят на практике.
▪️ Суть: один OS-поток на процесс. Runtime сам переключает контекст между задачами.
В CPython есть GIL (Global Interpreter Lock) — мьютекс, который позволяет выполнять байткод только одному потоку за раз. Это сделано для упрощения управления памятью через reference counting. В JavaScript модель изначально однопоточная — event loop обрабатывает задачи последовательно.
▪️Что это значит на практике
— Один инстанс сервиса = одно ядро процессора.
— Для использования 16 ядер нужно запустить 16 процессов + балансировщик.
— Потоки в Python/JS — это логическая абстракция, не настоящие OS threads.
— Для CPU-intensive задач в Python используют multiprocessing (отдельные процессы с изолированной памятью).
— Простой код без race conditions и deadlocks.
— Не нужны сложные примитивы синхронизации.
— Идеально для I/O-bound нагрузки (сеть, БД).
— Усложненная инфраструктура (больше процессов, межпроцессное взаимодействие через очереди/БД).
— Процессы изолированы — нет shared memory.
— Сложнее использовать многоядерность.
▪️ Суть: 1:1 mapping между Java Thread и OS thread. Планировщик ОС управляет всеми потоками.
Когда вы создаете new Thread() в классической Java, вы напрямую создаете OS thread. Все потоки конкурируют за CPU время через планировщик ОС. У каждого потока ~1 МБ стека.
▪️ Что это значит на практике
— Ограничение на количество потоков (~тысячи, не миллионы).
— Создание потока дорогая операция (~1ms).
— Context switch происходит в kernel space (затратно).
— Поэтому появились thread pools, чтобы переиспользовать потоки.
— Простая инфраструктура — один процесс использует все ядра.
— Shared memory между потоками (быстрый обмен данными).
— Race conditions, deadlocks, visibility/atomicity проблемы.
— Многопоточный код сложен в тестировании и отладке.
— Ограничение на количество потоков может стать узким местом даже при незагруженном CPU.
▪️ Суть: M:N mapping. Runtime сам мультиплексирует легковесные потоки на OS threads.
В Go это goroutines (управляются Go runtime через GMP-планировщик). В Java 21+ — Virtual Threads (Project Loom). Оба подхода реализуют концепцию green threads или user-level threads.
▪️ Механизм работы
Virtual Threads поверх ForkJoinPool.
— JVM создает carrier threads (platform threads).
— Виртуальные потоки привязываются/отвязываются от carrier threads.
— При блокирующей операции виртуальный поток отвязывается, carrier thread берет другой виртуальный поток.
— Стеки virtual threads хранятся в heap, управляются GC.
▪️Что это значит на практике
— Можно создавать миллионы потоков (в Go — goroutines, в Java — виртуальные потоки).
— Создание потока почти бесплатно (байты памяти вместо мегабайт).
— Блокирующий код работает эффективно — runtime просто переключает контекст на другую задачу.
— Планировщик делает принудительное переключение, предотвращая монополизацию CPU.
— Нет лимита на количество одновременных задач.
— Простой императивный код вместо async/reactive.
— Shared memory все еще доступна.
— Эффективное использование ресурсов.
— Проблемы многопоточности остаются (race conditions, deadlocks).
— В Java есть pinning проблема (synchronized блоки могут прибить virtual thread к carrier thread).
— Thread-local переменные нужно использовать осторожно.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥3🤔2
📌 6 принципов безопасности API
API — это не просто интерфейс, а ворота в вашу систему. Без должной защиты они становятся уязвимой точкой для атак.
🔐 Ключевые принципы безопасности API
— Использование HTTPS
— Аутентификация и авторизация
— Ограничение запросов
— Валидация входных данных
— Управление доступом на основе ролей (RBAC)
— Мониторинг и логирование
🔗 Подробнее в статье
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
API — это не просто интерфейс, а ворота в вашу систему. Без должной защиты они становятся уязвимой точкой для атак.
🔐 Ключевые принципы безопасности API
— Использование HTTPS
— Аутентификация и авторизация
— Ограничение запросов
— Валидация входных данных
— Управление доступом на основе ролей (RBAC)
— Мониторинг и логирование
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🔥1👏1
useful-java-links — там собраны лучшие материалы по Java. В репозитории найдёте библиотеки, фреймворки, инструменты и образовательные материалы по разным направлениям
🔹 Отдельный акцент сделан на решениях для работы с СУБД, поисковиками, технологиями обработки больших данных и ML. Список регулярно пополняется новыми ссылками и остаётся актуальным.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5🔥4👏1
🔥 Топ-10 интересных ошибок в реальных проектах
PVS-Studio собрали подборку самых интересных багов 2025 года из крупных Open Source проектов.
Тут есть всё: неочевидные языковые нюансы, которые ломают логику, ошибки из-за особенностей инициализации, проблемы с типами там, где их не ждёшь. Каждая ошибка разобрана с примерами кода и объяснением, почему она возникла.
Особенно понравится тем, кто любит копаться в деталях языка и понимать, как на самом деле работает JVM под капотом.
🔗 Читать на Habr
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
PVS-Studio собрали подборку самых интересных багов 2025 года из крупных Open Source проектов.
Тут есть всё: неочевидные языковые нюансы, которые ломают логику, ошибки из-за особенностей инициализации, проблемы с типами там, где их не ждёшь. Каждая ошибка разобрана с примерами кода и объяснением, почему она возникла.
Особенно понравится тем, кто любит копаться в деталях языка и понимать, как на самом деле работает JVM под капотом.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
😁5👍2🔥1
В статье рассмотрены 3 подхода к построению API. Также автор показывает, как объединить gRPC и GraphQL, чтобы фронт получал гибкие запросы, а бэк получал скорость и эффективность.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥2❤1👏1
Когда проект разрастается до десятков микросервисов, появляется ряд вопросов, которые не возникают, если у тебя монолит:
— Как сервису А узнать, где сейчас живёт сервис Б (тем более, если адреса постоянно меняются)?
— Как не утонуть в километрах кода для HTTP-запросов?
— Как фронтенду работать с этой кучей сервисов?
Для этого есть три главных инструмента.
В облаке сервисы постоянно перезапускаются, меняют IP-адреса и порты. Хардкодить http://localhost:8082 не вариант.
Eureka Server работает как справочная служба
🔹 При старте каждый сервис стучится в Eureka: «Я на связи, вот мой адрес».
🔹 Когда сервису А нужно достучаться до Б, он обращается к Eureka: «Подскажи, где сейчас сервис Б».
Нужно добавить аннотацию
@EnableDiscoveryClient для работы Eureka. Сервисы будут находить друг друга по имени, а не по IP.Для отправки запросов между сервисами можно использовать RestTemplate. Но код получается довольно громоздким и не типизированным.
Вместо этого можно использовать Feign. Он позволяет вызывать удаленный REST-сервис так, будто это обычный метод интерфейса. А вся реализация генерируется под капотом.
@FeignClient(name = "account-service") // Имя сервиса в Eureka
public interface AccountClient {
@GetMapping("/account/{userId}")
List<Account> getAccount(@PathVariable Long userId);
}
// Использование в сервисе:
List<Account> accounts = accountClient.getAccount(13L);
Теперь следующая проблема: сервисы между собой нормально общаются, а вот фронт всё ещё не знает кому точно отправлять запрос. Можно сложить на фронт адреса каждого сервиса, но это сложно и небезопасно.
Тут на помощь и приходит Spring Cloud Gateway. Он выступает как КПП. Фронт стучится только в него, а он уже сам разруливает запрос к нужным сервисам.
Например: запросы на /users/** идут в UserService.
🔹 Что он делает
— Маршрутизация: смотрит на путь запроса и решает, в какой именно сервис его отправить.
— Безопасность: проверяет JWT токен один раз на входе.
— Rate Limiting: защита от спама (например, максимум 10 запросов в секунду с одного IP).
1. Сервисы стартуют и сообщают Eureka о себе.
2. Фронт отправляет запрос в Gateway.
3. Gateway узнаёт у Eureka, где живёт нужный сервис, и перенаправляет запрос.
4. Когда сервисам нужно поговорить друг с другом, в дело вступает Feign.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15🔥3❤1👏1
Недавно в очередной раз столкнулся с холиваром: "Зачем ты переписал properties в yaml?" Давайте разбираться.
🔹 Properties — старая гвардия
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
🔹 YAML — современный подход
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
password: secret
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
🔹 Технические нюансы
— Парсинг и производительность: properties парсится через java.util.Properties (простой key-value), YAML требует SnakeYAML библиотеку. На старте приложения разница незаметна, но YAML чуть медленнее из-за построения объектной модели.
— Приоритет загрузки: Spring Boot загружает конфиги в порядке: application.properties → application.yml → application-{profile}.properties → application-{profile}.yml. Если есть оба формата, properties имеет приоритет над yaml для одинаковых ключей.
— Списки и массивы: в YAML естественная запись списков, в properties приходится использовать индексы.
— Placeholder resolution: оба формата поддерживают ${variable:default}, но в YAML нужно быть осторожным с экранированием двоеточий в URL.
— Невалидный синтаксис: properties молча проигнорирует строку без =, YAML упадёт с исключением при старте приложения — это и минус, и плюс одновременно.
— @PropertySource ограничение: аннотация работает только с .properties, для YAML нужно писать кастомный PropertySourceFactory.
YAML для основной конфигурации приложения — структура и удобство работы со сложными объектами того стоят. Properties для специфичных кейсов: custom property sources с @PropertySource, legacy интеграции, простые CI/CD переменные, конфиги для библиотек без Spring контекста.
В production проектах часто вижу гибридный подход: application.yml для базовой структуры + application-{env}.properties для переопределения специфичных параметров окружения через environment variables.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤4🔥1
Если ты хоть раз создавал Map<String, List<String>>, то пост для тебя.
Признай, каждый когда-нибудь писал:
Map<String, List<String>> userTags = new HashMap<>();
userTags.computeIfAbsent("user123", k -> new ArrayList<>()).add("premium");
userTags.computeIfAbsent("user123", k -> new ArrayList<>()).add("verified");
Или ещё хуже:
if (!userTags.containsKey("user123")) {
userTags.put("user123", new ArrayList<>());
}
userTags.get("user123").add("premium");Apache Commons Collections давно придумал MultiValuedMap.
🔹 Что это такое
MultiValuedMap<K, V> — это структура данных, которая позволяет хранить несколько значений для одного ключа. По сути, это Map<K, Collection<V>>, но с нормальным API.
MultiValuedMap<String, String> userTags = new ArrayListValuedHashMap<>();
userTags.put("user123", "premium");
userTags.put("user123", "verified");
userTags.put("user123", "early-adopter");
// Получаем все теги разом
Collection<String> tags = userTags.get("user123");
// [premium, verified, early-adopter]
Никаких computeIfAbsent, никаких проверок на null. Просто работает.
🔹 Что умеет
— Добавление без боли:
multiMap.put("key", "value1");
multiMap.put("key", "value2"); // не перезатирает предыдущее значение— Массовые операции:
multiMap.putAll("user456", Arrays.asList("admin", "moderator"));— Проверка наличия:
multiMap.containsMapping("user123", "premium"); // true/false— Удаление конкретного значения:
multiMap.removeMapping("user123", "premium");— Получение всех значений:
Collection<String> allTags = multiMap.values();
// все значения из всех ключей
🔹 Реализации
→ ArrayListValuedHashMap<K, V> — значения хранятся в ArrayList, порядок сохраняется, дубликаты разрешены
→ HashSetValuedHashMap<K, V> — значения хранятся в HashSet, без дубликатов
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥23👍11👏2❤1
Раньше switch был ограничен примитивами и enum'ами. С Java 21 Pattern Matching стал стандартом, и это принципиально меняет подход к обработке полиморфных данных.
Object obj = getObject();
String result;
if (obj instanceof String s) {
result = "String: " + s;
} else if (obj instanceof Integer i) {
result = "Int: " + i;
} else {
result = "Unknown";
}
String result = switch (obj) {
case String s -> "String: " + s;
case Integer i -> "Int: " + i;
case null -> "Null!";
default -> "Unknown";
};Но суть не в синтаксисе. Суть в гарантиях компилятора.
🔹 Охранные выражения (guarded patterns)
String classify(Object obj) {
return switch (obj) {
case String s when s.length() > 10 -> "Long string";
case String s -> "Short string";
case Integer i when i > 0 -> "Positive";
case Integer i -> "Non-positive";
case null -> "Null";
default -> "Other";
};
}Условия when проверяются последовательно. Компилятор отслеживает полноту покрытия и недостижимый код. Поменяете порядок кейсов неправильно — получите ошибку компиляции.
🔹 Record patterns — деструктуризация на месте
record Point(int x, int y) {}
String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == y ->
"Diagonal point";
case Point(int x, int y) ->
"Point at (%d, %d)".formatted(x, y);
default -> "Not a point";
};
}Распаковали record прямо в case. Никаких геттеров, никаких промежуточных переменных.
🔹 Sealed классы + pattern matching = полнота проверок
sealed interface Result permits Success, Failure {}
record Success(String data) implements Result {}
record Failure(String error) implements Result {}
String handle(Result result) {
return switch (result) {
case Success(String data) -> "Got: " + data;
case Failure(String error) -> "Error: " + error;
// default не нужен - компилятор знает все варианты
};
}Компилятор гарантирует, что вы обработали все случаи. Добавите новый класс в sealed иерархию — код не скомпилится, пока не обработаете его.
JIT оптимизирует pattern matching свитчи агрессивно. В бенчмарках разница с if-else цепочками от 2x до 10x в пользу switch в зависимости от количества веток.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14👍7❤1⚡1👏1
Каждый раз, когда вы набираете https://example.com в браузере, за кулисами запускается целая цепочка событий.
Браузер делит URL на части:
▪️ https — протокол
▪️ example.com — домен
▪️ /page — путь к ресурсу
Если IP не сохранён в кеше, браузер спрашивает DNS-сервер: «Где живёт example.com?»
Создаётся TCP-соединение с сервером по IP и порту (80 для HTTP, 443 для HTTPS).
Браузер отправляет HTTP-запрос: GET /page HTTP/1.1
Сервер возвращает HTML, CSS, JS и статус-код (например, 200 OK или 404 Not Found).
Браузер обрабатывает HTML, применяет стили и выполняет JavaScript.
Если сайт работает по HTTPS, соединение шифруется через SSL/TLS.
Браузер сохраняет ресурсы, чтобы при следующем визите всё грузилось быстрее.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤2🔥1👏1
🔹 RestTemplate
javaRestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> response = restTemplate.getForEntity(
"https://api.example.com/users/{id}",
User.class,
userId
);
User user = response.getBody();
🔹 WebClient
javaWebClient webClient = WebClient.create("https://api.example.com");
Mono<User> userMono = webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);🔹 Технические нюансы
— Thread model: RestTemplate использует по потоку на запрос (blocking I/O). WebClient работает на event loop с малым числом потоков (по умолчанию CPU cores * 2).
— Performance: в синхронных сценариях разница минимальна. При параллельных запросах WebClient показывает x2-x5 прирост throughput за счёт эффективного использования потоков.
— Memory footprint: 1000 параллельных REST вызовов через RestTemplate = ~1000 МБ стека потоков. WebClient с той же нагрузкой — десятки МБ.
— Timeout configuration: RestTemplate требует настройки через ClientHttpRequestFactory. WebClient имеет встроенный .timeout(Duration) в цепочке вызовов.
— Error handling: RestTemplate выбрасывает исключения синхронно. WebClient возвращает Mono с ошибкой.
— Testing: RestTemplate легко мокается через MockRestServiceServer. WebClient требует понимания StepVerifier из reactor-test.
— Compatibility: RestTemplate работает везде. WebClient требует Spring WebFlux в classpath, даже если используешь его в обычном Spring MVC приложении.
WebClient если стартуешь новый проект, делаешь микросервисы с высокой нагрузкой, уже используешь reactive stack или планируешь масштабироваться. RestTemplate если поддерживаешь legacy код без реактивности, команда не знакома с Project Reactor, делаешь простой CRUD сервис с малой нагрузкой.
Главное помни: преждевременная оптимизация — корень зла. Если RestTemplate закрывает задачу, не усложняй. Но если упираешься в потоки на проде, время учить реактивщину.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🔥3❤2👏1
This media is not supported in your browser
VIEW IN TELEGRAM
Платформа с реальными DevOps-задачами. Кейсы по Linux, Docker и Kubernetes — всё как в бою, но с читами:
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5🔥4👏1
🔬 Правильный вопрос про AI
Все исследования AI-ассистентов меряют одно: насколько быстрее ты закрываешь задачу. Метрика удобная, но она игнорирует 70–80% реальных затрат — поддержку, рефакторинг, устранение дефектов.
Исследование «Echoes of AI» (arXiv, 2025) спросило другое:
«Что будет, когда другой разработчик возьмёт AI-код и попытается его развивать?»
151 участник, 95% практикующие разработчики (не студенты). Java/Spring Boot проект, две фазы, настоящее РКИ. Одна группа пишет с AI, другая без. Потом третья группа без AI поддерживает и тот, и другой код.
Результат оказался неожиданный.
👉 Читать
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
Все исследования AI-ассистентов меряют одно: насколько быстрее ты закрываешь задачу. Метрика удобная, но она игнорирует 70–80% реальных затрат — поддержку, рефакторинг, устранение дефектов.
Исследование «Echoes of AI» (arXiv, 2025) спросило другое:
«Что будет, когда другой разработчик возьмёт AI-код и попытается его развивать?»
151 участник, 95% практикующие разработчики (не студенты). Java/Spring Boot проект, две фазы, настоящее РКИ. Одна группа пишет с AI, другая без. Потом третья группа без AI поддерживает и тот, и другой код.
Результат оказался неожиданный.
👉 Читать
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤1🔥1
🧩 Просто о сложном: Saga Pattern
Когда пользователь оформляет заказ, нужно списать деньги, зарезервировать товар и уведомить склад. Всё в разных сервисах. Что делать, если один из них упал?
Обычная транзакция тут не поможет. Добро пожаловать в Saga.
Идея проста: разбиваем большую распределённую транзакцию на цепочку маленьких локальных. Каждый шаг — это своя транзакция в своём сервисе. Если что-то пошло не так, то запускаем компенсирующие транзакции в обратном порядке.
🔹 Два подхода
1. Choreography (хореография)
Сервисы общаются через события. Каждый сам знает, что делать дальше.
✔️ Просто, нет единой точки отказа
❌ Сложно отследить весь флоу, спагетти из событий
2. Orchestration (оркестрация)
Есть дирижёр — Saga Orchestrator. Он знает весь сценарий и командует сервисами.
✔️ Флоу виден в одном месте, легко дебажить
❌ Оркестратор может стать узким местом
⚠️ Важно понимать: компенсирующая транзакция ≠ откат БД. Это бизнес-операция. Если деньги уже списаны, то мы не откатываем строку в БД, мы делаем возврат.
В Java-экосистеме Saga используют вместе с:
→ Apache Kafka / RabbitMQ — для событий между сервисами
→ Axon Framework — встроенная поддержка Saga из коробки
→ Spring State Machine — для управления состоянием оркестратора
→ Temporal / Conductor — оркестрация workflow на уровне инфраструктуры
🔹 Когда использовать
✔️ Микросервисная архитектура
✔️ Несколько БД, нет возможности использовать 2PC
✔️ Долгоживущие бизнес-транзакции
❌ Монолит с одной БД — просто используй @Transactional
Saga — это не серебряная пуля, это компромисс. Ты жертвуешь изоляцией ради масштабируемости. Данные могут быть временно не консистентны и это нормально, если бизнес с этим согласен.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava
Когда пользователь оформляет заказ, нужно списать деньги, зарезервировать товар и уведомить склад. Всё в разных сервисах. Что делать, если один из них упал?
Обычная транзакция тут не поможет. Добро пожаловать в Saga.
Идея проста: разбиваем большую распределённую транзакцию на цепочку маленьких локальных. Каждый шаг — это своя транзакция в своём сервисе. Если что-то пошло не так, то запускаем компенсирующие транзакции в обратном порядке.
🔹 Два подхода
1. Choreography (хореография)
Сервисы общаются через события. Каждый сам знает, что делать дальше.
OrderService → [OrderCreated] → PaymentService
PaymentService → [PaymentDone] → InventoryService
InventoryService → [Reserved] → NotificationService
2. Orchestration (оркестрация)
Есть дирижёр — Saga Orchestrator. Он знает весь сценарий и командует сервисами.
public class OrderSagaOrchestrator {
public void execute(Order order) {
try {
paymentService.charge(order);
inventoryService.reserve(order);
notificationService.notify(order);
} catch (PaymentException e) {
// компенсация не нужна — деньги не списаны
} catch (InventoryException e) {
paymentService.refund(order); // компенсируем
}
}
}В Java-экосистеме Saga используют вместе с:
→ Apache Kafka / RabbitMQ — для событий между сервисами
→ Axon Framework — встроенная поддержка Saga из коробки
→ Spring State Machine — для управления состоянием оркестратора
→ Temporal / Conductor — оркестрация workflow на уровне инфраструктуры
🔹 Когда использовать
Saga — это не серебряная пуля, это компромисс. Ты жертвуешь изоляцией ради масштабируемости. Данные могут быть временно не консистентны и это нормально, если бизнес с этим согласен.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤2🔥2👏1👾1
Наглядная шпаргалка с примерами и визуализацией SQL JOIN.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14❤6⚡2🔥2
Мне НЕ нужно разбираться с управлением памятью в Java, так как за меня всё делает GC
Garbage Collector (GC) действительно очищает неиспользуемые объекты, которые больше не имеют активных ссылок. Благодаря этому в Java не нужно вручную освобождать память, как в C++.
Несколько предпосылок к исходному тезису:
🔹 В других языках, где ручное управление памятью (C, C++), утечки очевидны — если забыл free(), память навсегда потеряна.
🔹 В Java GC работает автоматически, поэтому кажется, что он решает все проблемы сам.
🔹 "Ну раз GC есть, значит, про память можно не думать!" — типичная ошибка.
GC удаляет только те объекты, которые больше не имеют активных ссылок. Если же объект остаётся доступным, но фактически не используется, он будет занимать память до завершения работы приложения.
Если создать static List и постоянно добавлять в него объекты, они никогда не будут удалены GC, потому что статические поля живут весь срок жизни приложения.
public class MemoryLeak {
private static final List<byte[]> cache = new ArrayList<>();
public static void main(String[] args) {
while (true) {
cache.add(new byte[10 * 1024 * 1024]);
System.out.println("Добавили 10MB в кеш. Используемая память: " +
(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + "MB");
}
}
}Через пару минут — OutOfMemoryError
Объекты, хранящиеся в ThreadLocal, привязываются к потоку, а в пуле потоков они могут жить дольше, чем нужно.
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> threadLocalData = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
threadLocalData.set(new byte[10 * 1024 * 1024]); // 10MB на поток
System.out.println("Память занята потоком!");
});
}
executor.shutdown();
}
}Поток завершится, а память останется занята, потому что ThreadLocal не очищается автоматически.
Если анонимный класс или лямбда-ссылка ссылается на внешний объект, она может мешать GC очистить его.
public class InnerClassLeak {
private String data = "Очень важные данные";
public void createAnonymousClass() {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Используем: " + data);
}
};
new Thread(task).start();
}
}task ссылается на data, даже если InnerClassLeak больше не используется → GC не очистит объект.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤5🔥3
Если производительность внезапно падает, хотя два потока работают с разными частями одного объекта, скорее всего, проблема в так называемом false sharing.
Вот как это работает: процессор не берет данные из памяти побайтово. Он работает с cache line, обычно это 64 байта. Если два ядра процессора держат одну и ту же cache line в своих кэшах первого уровня (L1) и хотя бы одно ядро в нее пишет, запускается протокол когерентности (MESI). Этот протокол делает копию cache line недействительной в другом ядре. И это происходит, даже если ядра пишут в разные поля объекта, но эти поля физически находятся в одной cache line.
🔹 Как объекты лежат в памяти
JVM (HotSpot) раскладывает объект примерно так:
[mark word — 8 байт]
[klass pointer — 4/8 байт]
[поля по убыванию размера: long/double → int/float → short/char → byte/boolean → ссылки]
[padding до выравнивания на 8 байт]
Порядок полей в исходнике не соответствует порядку в памяти. JVM перегруппировывает их для минимизации выравнивания. Проверить реальный layout можно через JOL:
javaSystem.out.println(ClassLayout.parseClass(MyClass.class).toPrintable());
Для класса с `long`, `int` и `boolean` JOL покажет что-то вроде:
OFFSET SIZE TYPE
0 4 (object header: mark)
4 4 (object header: mark)
8 4 (object header: class)
12 4 int value
16 8 long counter
24 1 boolean flag
25 7 (alignment/padding)
Итого 32 байта — меньше cache line. Два таких объекта рядом в массиве легко попадут в одну линию.
🔹 Классический сценарий
class Counters {
volatile long a;
volatile long b;
}Поля a и b занимают по 8 байт. Заголовок объекта – примерно 12-16 байт. Вероятно, оба поля попадают в одну 64-байтную строку кэша. Если два потока независимо увеличивают a и b, они постоянно будут делать строку недействительной друг для друга. На многоядерной системе это легко может привести к падению производительности в 5-10 раз по сравнению с независимыми счетчиками.
Решение – добавление отступов (padding). До Java 8 это делали руками, добавляя фиктивные поля:
class PaddedCounter {
volatile long value;
long p1, p2, p3, p4, p5, p6, p7; // 56 байт padding
}Уродливо и ненадёжно. Плюс JIT мог выкинуть «мёртвые» поля.
С Java 8 появилась аннотация @Contended (пакет jdk.internal.vm.annotation):
@Contended
volatile long a;
@Contended
volatile long b;
В JVM автоматически добавляется padding в 128 байт вокруг помеченного поля. Это сделано для защиты от prefetcher'а (размер двойной cache line). Чтобы это работало вне классов JDK, нужно использовать опцию -XX:-RestrictContended.
Аннотация @Contended стоит на полях внутри Thread (например, для thread-local random), ForkJoinPool и Striped64, который является базовым классом для LongAdder и LongAccumulator.
🔹 LongAdder как способ решения проблемы
LongAdder показывает, как можно решить проблему не просто добавлением отступов, а с помощью особого подхода к проектированию. Вместо одного volatile long используется массив Cell[], где у каждой ячейки стоит @Contended. Каждый поток пишет в свою ячейку, итоговое значение получается через sum(). При большой нагрузке это работает намного быстрее, чем AtomicLong.
За это приходится платить: sum() не является атомарной операцией. Если нужна полная точность в момент чтения, LongAdder не подойдет.
🔹 О массивах примитивных типов
Тут есть свои нюансы с long[]. Элементы располагаются один за другим, без каких-либо дополнительных данных между ними. Восемь элементов long занимают ровно одну cache line. Если несколько потоков одновременно пишут в соседние ячейки, false sharing практически гарантирован. Именно поэтому в Striped64 используется массив объектов Cell[], а не long[]: каждый объект с @Contended изолирован от других.
══════ Навигация ══════
Вакансии • Задачи • Собесы
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12🔥6❤4👏1