Chronicle Map для Java: полное руководство с примерами
Chronicle Map — это высокопроизводительная, off-heap (вне кучи) реализация ConcurrentMap для Java, разработанная компанией Chronicle Software. В отличие от стандартных HashMap или ConcurrentHashMap, которые хранят данные в куче JVM (heap), Chronicle Map размещает данные непосредственно в оперативной памяти за пределами управляемой кучи. Это позволяет избежать накладных расходов, связанных с сборкой мусора (GC), и обеспечивает предсказуемую задержку даже при работе с большими объёмами данных (гигабайты и терабайты).
Эта библиотека идеально подходит для сценариев, где требуется низкая задержка, высокая пропускная способность и работа с данными, которые не должны влиять на время пауз GC. Типичные применения: финансовые системы (торговые платформы), кэширование, системы реального времени, хранение сессий, а также любые приложения, где важна детерминированная производительность. Chronicle Map поддерживает многопоточный доступ, сериализацию ключей и значений, а также может быть сконфигурирована для персистентности (сохранения данных на диск).
В этой статье мы подробно рассмотрим установку, основные методы, приведём множество рабочих примеров и дадим советы по производительности. К концу вы сможете уверенно использовать Chronicle Map в своих Java-проектах.
1. Установка
Chronicle Map распространяется через Maven Central. Для использования в вашем проекте добавьте следующую зависимость в файл pom.xml (Maven):
<dependency>
<groupId>net.openhft</groupId>
<artifactId>chronicle-map</artifactId>
<version>3.25ea0</version> <!-- Проверьте актуальную версию на Maven Central -->
</dependency>
Если вы используете Gradle, добавьте строку в build.gradle:
implementation 'net.openhft:chronicle-map:3.25ea0'
2. Быстрый старт — минимальный рабочий пример
Создадим простую карту, которая хранит пары "имя пользователя" (String) и "количество входов" (Integer).
import net.openhft.chronicle.map.ChronicleMap;
public class QuickStart {
public static void main(String[] args) {
// 1. Конфигурация и создание карты
ChronicleMap<String, Integer> userLoginCount = ChronicleMap
.of(String.class, Integer.class)
.name("user-login-map")
.averageKey("user") // Средняя длина ключа для оптимизации
.averageValue(1) // Среднее значение для оптимизации
.entries(1000) // Ожидаемое количество записей
.create();
// 2. Вставка данных
userLoginCount.put("alice", 5);
userLoginCount.put("bob", 3);
userLoginCount.put("charlie", 10);
// 3. Чтение данных
System.out.println("Входы alice: " + userLoginCount.get("alice"));
System.out.println("Входы bob: " + userLoginCount.get("bob"));
// 4. Итерация (осторожно, может быть медленной)
userLoginCount.forEach((key, value) ->
System.out.println(key + " -> " + value));
// 5. Закрытие карты (освобождение off-heap памяти)
userLoginCount.close();
}
}
Этот пример демонстрирует базовую работу: создание, вставка, чтение и итерация. Обратите внимание на методы averageKey() и averageValue() — они критически важны для эффективного использования памяти.
3. Основные методы и классы
Рассмотрим ключевые методы ChronicleMap. Все они являются частью интерфейса ChronicleMap, который расширяет java.util.concurrent.ConcurrentMap.
3.1. ChronicleMap.of(Class<K>, Class<V>)
Сигнатура: public static <K, V> ChronicleMapBuilder<K, V> of(Class<K> keyClass, Class<V> valueClass)
Описание: Статический фабричный метод, который возвращает билдер для настройки карты. Это единственный способ создать экземпляр ChronicleMap. Билдер позволяет задать все параметры: размер, средние значения, сериализаторы и т.д.
Пример:
ChronicleMapBuilder<Long, String> builder = ChronicleMap.of(Long.class, String.class);
ChronicleMap<Long, String> map = builder
.name("my-map")
.averageValue("some-average-string")
.entries(10_000)
.create();
3.2. put(K key, V value)
Сигнатура: V put(K key, V value)
Описание: Вставляет или обновляет запись. Возвращает предыдущее значение, связанное с ключом, или null, если ключа не было. Работает атомарно.
Пример:
ChronicleMap<String, String> map = ...;
String old = map.put("key1", "value1"); // null
String old2 = map.put("key1", "value2"); // "value1"
3.3. get(Object key)
Сигнатура: V get(Object key)
Описание: Возвращает значение, связанное с ключом, или null, если ключ отсутствует. Важно: возвращаемое значение — это копия данных из off-heap памяти, а не ссылка на внутреннее хранилище.
Пример:
ChronicleMap<Integer, Double> map = ...;
map.put(1, 3.14);
Double pi = map.get(1); // 3.14
Double missing = map.get(2); // null
3.4. remove(Object key)
Сигнатура: V remove(Object key)
Описание: Удаляет запись по ключу. Возвращает удалённое значение или null, если ключ не найден. Операция атомарна.
Пример:
ChronicleMap<String, Integer> map = ...;
map.put("temp", 42);
Integer removed = map.remove("temp"); // 42
Integer removedAgain = map.remove("temp"); // null
3.5. containsKey(Object key)
Сигнатура: boolean containsKey(Object key)
Описание: Проверяет, существует ли запись с указанным ключом. Возвращает true или false. Этот метод может быть быстрее, чем get(), если значение не нужно.
Пример:
ChronicleMap<UUID, String> map = ...;
UUID id = UUID.randomUUID();
map.put(id, "data");
if (map.containsKey(id)) {
System.out.println("Ключ найден!");
}
3.6. putIfAbsent(K key, V value)
Сигнатура: V putIfAbsent(K key, V value)
Описание: Атомарно вставляет значение, только если ключ отсутствует. Возвращает существующее значение, если ключ уже есть, или null в случае успешной вставки.
Пример:
ChronicleMap<String, String> cache = ...;
String result = cache.putIfAbsent("config", "default");
if (result == null) {
System.out.println("Значение установлено");
} else {
System.out.println("Уже было: " + result);
}
3.7. replace(K key, V oldValue, V newValue)
Сигнатура: boolean replace(K key, V oldValue, V newValue)
Описание: Атомарно заменяет значение, только если текущее значение равно oldValue. Используется для оптимистичных блокировок (CAS-операции).
Пример:
ChronicleMap<String, Integer> counter = ...;
counter.put("visits", 10);
boolean replaced = counter.replace("visits", 10, 11); // true
boolean failed = counter.replace("visits", 10, 12); // false, т.к. сейчас 11
3.8. forEach(BiConsumer<? super K, ? super V> action)
Сигнатура: void forEach(BiConsumer<? super K, ? super V> action)
Описание: Выполняет заданное действие для каждой записи в карте. Важно: итерация по ChronicleMap может быть медленной, так как требует десериализации каждого ключа и значения. Не используйте для больших карт в критических к производительности участках.
Пример:
ChronicleMap<String, Double> prices = ...;
prices.put("apple", 1.2);
prices.put("banana", 0.8);
prices.forEach((key, value) ->
System.out.println("Товар: " + key + ", цена: " + value));
3.9. close()
Сигнатура: void close()
Описание: Закрывает карту и освобождает всю связанную с ней off-heap память. После вызова close() карта становится непригодной для использования. Рекомендуется вызывать в блоке finally или использовать try-with-resources.
Пример:
try (ChronicleMap<Integer, String> map = ChronicleMap
.of(Integer.class, String.class)
.name("temp-map")
.entries(100)
.create()) {
map.put(1, "one");
// Автоматически закроется при выходе из блока try
}
3.10. size()
Сигнатура: int size()
Описание: Возвращает количество записей в карте. В отличие от ConcurrentHashMap, этот метод не блокирует карту, но может возвращать неточное значение при одновременных модификациях.
Пример:
ChronicleMap<String, String> map = ...;
map.put("a", "1");
map.put("b", "2");
System.out.println("Размер: " + map.size()); // 2
4. Реальный практический пример: Кэш сессий пользователей
Представим, что мы разрабатываем высоконагруженный веб-сервер, которому необходимо хранить сессии пользователей. Сессия содержит ID пользователя, время последней активности и некоторые метаданные. Использование Chronicle Map позволит нам хранить миллионы сессий без нагрузки на GC.
import net.openhft.chronicle.map.ChronicleMap;
import java.io.Serializable;
import java.time.Instant;
import java.util.UUID;
// Класс, представляющий данные сессии
class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
final UUID userId;
final String userName;
volatile Instant lastAccess;
final String metadata;
public UserSession(UUID userId, String userName, String metadata) {
this.userId = userId;
this