Сколько сегментов создается по умолчанию в ConcurrentHashMap?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
ConcurrentHashMap: Сегменты и внутренняя структура
ConcurrentHashMap в Java 7 и ранее использовала segments для распределения lock'ов, но в Java 8+ архитектура была полностью переработана. Давайте разберемся в обоих подходах.
Java 7 и ранее: Segment-based architecture
В старых версиях ConcurrentHashMap имела по умолчанию 16 сегментов:
// Java 7
private static final int DEFAULT_SEGMENTS = 16;
private static final int SEGMENT_SHIFT = 28;
private static final int SEGMENT_MASK = 15; // 16 - 1 = 0xF
public class ConcurrentHashMap<K, V> {
final Segment[] segments;
public ConcurrentHashMap() {
// По умолчанию 16 сегментов
this(16); // DEFAULT_INITIAL_CAPACITY = 16
}
public ConcurrentHashMap(int initialCapacity) {
int ssize = 1;
int c = concurrencyLevel / ssize; // concurrencyLevel = 16 по умолчанию
while (ssize < concurrencyLevel) {
ssize <<= 1;
}
segmentShift = 32 - Integer.numberOfLeadingZeros(ssize - 1);
segmentMask = ssize - 1;
this.segments = new Segment[ssize];
for (int i = 0; i < segments.length; ++i) {
segments[i] = new Segment<>(initialCapacity / ssize + 1);
}
}
}
Каждый Segment — это отдельный lock и своя bucket таблица:
static class Segment<K, V> extends ReentrantLock {
transient volatile int modCount;
transient int threshold;
transient HashEntry<K, V>[] table; // Своя таблица для каждого сегмента
transient int count;
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // Lock этого сегмента
try {
// Только этот сегмент заблокирован
// Другие сегменты могут работать параллельно
} finally {
unlock();
}
}
}
Java 8 и позже: Node-based architecture с CAS операциями
Отправляясь от Java 8, ConcurrentHashMap была полностью переработана и больше не использует сегменты:
// Java 8+
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> {
// Вместо segments
transient volatile Node<K, V>[] table; // Единая таблица
// Для очередного incrementSizeCtl при resizing
private transient volatile int sizeCtl;
public ConcurrentHashMap() {
// Нет сегментов по умолчанию!
// Таблица не создаётся до первого put()
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException();
}
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap; // Используем sizeCtl для lazy initialization
}
}
Ключевая разница: Java 8+ использует node-level locking с CAS (Compare-And-Swap) вместо segment-level locking:
// Java 8+ Node structure
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
volatile V val;
volatile Node<K, V> next; // Для collision chains
Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
}
// CAS операция вместо segment lock
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
// Получаем нужный bucket
Node<K, V>[] tab = table;
if (tab == null || (tab = initTable()) == null) {
tab = initTable();
}
int index = (tab.length - 1) & hash;
Node<K, V> f;
// Синхронизируем только нужный node (bucket)
if ((f = tabAt(tab, index)) == null) {
// CAS операция вместо lock
if (casTabAt(tab, index, null, new Node<K, V>(hash, key, value)))
break;
} else if (f.hash == MOVED) {
tab = helpTransfer(tab, f);
} else {
synchronized (f) { // Lock только этого node'a
// Update logic
}
}
return null;
}
// Методы для CAS
private static final <K, V> Node<K, V> tabAt(
Node<K, V>[] tab, int i) {
return (Node<K, V>) U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
private static final <K, V> boolean casTabAt(
Node<K, V>[] tab, int i, Node<K, V> c, Node<K, V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
Сравнение: Java 7 vs Java 8+
| Параметр | Java 7 | Java 8+ |
|---|---|---|
| Granularity | Segment-level (16 по умолчанию) | Node-level (bucket-level) |
| Locking | ReentrantLock на сегмент | Synchronized или CAS на node |
| Concurrency | Max 16 потоков одновременно | Max table.length потоков |
| Performance | Хорошо для многопоточности | Лучше, меньше contention |
| Memory | Больше overhead на segments | Меньше overhead |
| Resize | Resize сегмента | Resize всей таблицы |
Практические примеры
Java 7: Segment-based
// Два потока на разных сегментах могут работать одновременно
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Thread 1: Работает с сегментом 0
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key_" + i, i); // i % 16 определяет сегмент
}
});
// Thread 2: Работает с сегментом 1
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key_" + (1000 + i), 1000 + i);
}
});
// Если ключи распределяются по разным сегментам, работают параллельно
// Максимум 16 потоков могут писать одновременно
Java 8+: Node-based
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Гораздо больше потоков могут работать параллельно
// Потому что каждый bucket имеет свой lock
// Thread 1: Пишет в bucket 0
Thread t1 = new Thread(() -> {
map.put("key_a", 1);
});
// Thread 2: Пишет в bucket 1 (может работать одновременно, в Java 8+)
Thread t2 = new Thread(() -> {
map.put("key_b", 2);
});
// В Java 8+, даже 32 потока могут работать параллельно
// если таблица достаточно большая
Сегменты в контексте конкурентности
// Правила для хеширования в ConcurrentHashMap
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// Размер таблицы ВСЕГДА power of 2
// Начальный размер: 16 (в обоих Java 7 и 8+)
int cap = tableSizeFor(16); // -> 16
int cap2 = tableSizeFor(20); // -> 32
// Java 7: 16 сегментов на таблице размером 16 = 1 bucket на сегмент
// Java 8+: Таблица может resized до нужного размера
Мониторинг размера таблицы в Java 8+
public class ConcurrentHashMapInspector {
public static <K, V> int getTableSize(ConcurrentHashMap<K, V> map) throws Exception {
Field f = ConcurrentHashMap.class.getDeclaredField("table");
f.setAccessible(true);
Object[] table = (Object[]) f.get(map);
return table == null ? 0 : table.length;
}
public static void main(String[] args) throws Exception {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
System.out.println("Initial table size: " + getTableSize(map)); // 0, lazy init
map.put("key1", 1);
System.out.println("After first put: " + getTableSize(map)); // 16
// Добавляем элементы до resize
for (int i = 0; i < 100; i++) {
map.put("key" + i, i);
}
System.out.println("After 100 puts: " + getTableSize(map)); // 64 или 128
// Таблица растёт dynamically
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
System.out.println("After 1000 puts: " + getTableSize(map)); // 2048 или больше
}
}
Ответ на вопрос
Java 7 и ранее: ConcurrentHashMap по умолчанию имеет 16 сегментов.
Java 8 и позже: ConcurrentHashMap не использует сегменты. Вместо этого используется node-level locking и CAS операции для лучшей конкурентности. Таблица может расти dynamically в зависимости от нагрузки (начиная с 16, потом 32, 64, 128 и т.д.).
Практический вывод: В современной Java (8+) ConcurrentHashMap гораздо лучше масштабируется чем в Java 7, потому что может обслуживать гораздо больше потоков одновременно.