← Назад к вопросам

Сколько сегментов создается по умолчанию в ConcurrentHashMap?

2.0 Middle🔥 151 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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 7Java 8+
GranularitySegment-level (16 по умолчанию)Node-level (bucket-level)
LockingReentrantLock на сегментSynchronized или CAS на node
ConcurrencyMax 16 потоков одновременноMax table.length потоков
PerformanceХорошо для многопоточностиЛучше, меньше contention
MemoryБольше overhead на segmentsМеньше overhead
ResizeResize сегмента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, потому что может обслуживать гораздо больше потоков одновременно.

Сколько сегментов создается по умолчанию в ConcurrentHashMap? | PrepBro