JDK7中的HashMap与ConcurrentHashMap

一、 JDK7中的HashMap

1. 数据结构

  • 数组+链表
  • 链地址法解决Hash冲突;
  • 链表没有头结点,链表的第一个元素放在数组里,后续的元素用next指针连接起来;

2. 属性

2.1 成员变量源码

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    // 静态常量:数组的默认初始容量,必须是2的幂次方,初始值16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 静态常量:数组的最大容量,2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 静态常量:默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 静态常量:空数组对象
    static final Entry<?,?>[] EMPTY_TABLE = {};

    // 分配的数组空间,长度必须是2的幂次方
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    // map中key-value的节点总数,包括在链表中的节点
    transient int size;

    // 触发map扩容的元素总数阈值
    int threshold;

    // map的加载因子
    final float loadFactor;

    // map中`key-value`添加或删除的次数
    transient int modCount;

    // 开启rehash的阈值的默认值
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

    /**
     * 计算hash值使用的hash种子
     */
    transient int hashSeed = 0;
    
    // 省略其他
    // ...
}
  • transient关键字的作用是序列化对象时,不序列化该属性;

2.2 加载因子

  • 加载因子 = HashMap中存在的元素总数 / HashMap中数组长度;
  • 注意:元素总数包括在链表上的元素数,数组长度是指当前分配的数组空间的大小;
  • 加载因子默认是0.75

2.3 modCount

  • modCount记录Map中key-value添加或删除元素的次数,(修改key对应的value值不会影响modCount值);
  • 该属性的值作用是在使用迭代器遍历map时,如果元素个数有修改,则快速失败;
  • 如果在迭代过程中需要删除或增加map中元素,可以使用迭代器的删除或增加方法;

3. 初始化

  • 初始化时数组长度为16
  • 构造方法可以指定初始化大小size,但是在具体初始化时会初始化为大于size且值为2的幂次方的最小值;
  • HashMap的构造方法只是执行给成员变量赋值,调用的初始化方法init()为空方法,所以初始化时并未分配空间;
  • 在添加元素调用put方法时,会判断数组是否为空,如果为空,这时才去初始化数组;

3.1 构造方法初始化变量

构造方法只是初始化成员变量,并没有分配数组空间:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();
}


void init() {
}
  • 构造方法先初始化成员变量,然后调用init方法;
  • init方法为空方法,说明调用构造方法时并没有分配空间;

3.2 put第一个元素时分配空间

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    
    // 省略其他处理,put方法详细见下文
    // ...
}
  • put元素时,如果数组未分配空间,则通过inflateTable方法分配空间,未分配空间时,threshold值为初始化大小;
inflateTable方法:
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
  • 真实初始化的容量capacity会通过roundUpToPowerOf2方法计算出大于toSize且为2的幂次方的最小值;
  • 然后通过table = new Entry[capacity]初始化数组;
roundUpToPowerOf2方法
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
  • roundUpToPowerOf2方法会调用Integer类的highestOneBit方法计算小于number且值为2的幂次方的最大值;
  • 参数里number-1是当number刚好为2的幂次方时,计算出来的就是number;

4. 添加元素

4.1 静态内部类Entry

Entry对象封装了要添加的节点,包含keyvaluenexthash属性:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    
    // 省略成员方法
    // ...
}
  • final修饰keykey值不可修改;
  • next属性用来构建链表;
  • Entry里会记录当前key计算的hash值;

4.2 添加元素步骤

  1. 如果数组为空,则初始化;
  2. 根据key计算hash值hash = hash(key);
  3. 根据hash计算数组下标index = hash(key)&(length-1)
  4. 根据index找到链表,遍历链表,查找key值是否已经存在,如果存在则替换value值,返回旧的值oldValue
  5. 如果key值未存在,则将新的key-value生成的Entry节点插入链表头部
put方法

put方法负责放入key-value

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
  • 初始化put方法里会判断数组是否为空,如果为空,则进行初始化,即map空间是在添加第一个元素时进行的初始化;构造方法只负责设置成员变量的值;
  • 同key覆盖put方法会先循环变量key对应的下标下的链表,如果key值已经存在,则替换value,将旧的值oldValue返回;
  • 新key加节点:如果key值不存在,则使用addEntry方法向链表添加新元素,添加时使用的是头插法
  • key为null处理:如果key==null,则直接调用putForNullKey方法处理放入keynull
addEntry方法

addEntry方法将新的key-value放入map:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

添加元素之前,会先判断是否需要扩容,扩容条件是(size >= threshold) && (null != table[bucketIndex]),即需要同时满足这两个条件才能扩容:

  • 条件1:当前元素总量大于等于扩容阈值,扩容阈值=数组长度*加载因子
  • 条件2:当前key值计算的数组下标中元素不为空,即当前要添加的key-value值由hash冲突;
createEntry方法

createEntry方法添加元素:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
  • table[bucketIndex] = new Entry<>(hash, key, value, e);该行代码使用的是头插法;
putForNullKey方法

putForNullKey方法,负责添加Key为null节点:

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
  • HashMap支持Key为null的节点,该节点存储在数组下标为0的链表中;

4.3 根据key值计算数组下标

计算hash值

hash方法计算key的hash值:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
  • 最终调用的是ObjecthashCode方法计算key的hash值;
  • 移位异或是进行折叠运算,使hash值的高位都参与到计算index里(因为计算index是通过与数组length位运行,length数字可能很小,hash折叠一下可以让高位也参与到index的计算,从而提高hash散列性);
计算index

indexFor方法计算下标

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
  • hash值与数组长度减一lenght-1进行按位或运算,效果就是对数组长度取模;
  • 数组length必须是2的幂次方,位运算才能达到取模的效果;
  • 位运算速度更快;

5. 扩容

5.1 扩容步骤

  1. 添加新节点时,根据扩容条件判断是否需要扩容;
  2. 如果需要扩容,调用resize方法进行扩容,新的table长度为当前table的2倍;
  3. 创建完新的table后,调用transfer方法,将原来table中的元素进行移动,根据initHashSeedAsNeeded(newCapacity)方法返回值判断是否需要重新hash,如果不需要rehash,则直接按照Entry里存放的hash计算index
addEntry方法判断扩容条件

添加新元素时,会判断是否需要扩容,扩容条件在addEntry方法里判断

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

根据if条件可以看到,扩容需要同时满足两个条件:

  • 条件1:当前元素总量大于等于扩容阈值,扩容阈值=数组长度*加载因子
  • 条件2:当前key值计算的数组下标中元素不为空,即当前要添加的key-value有hash冲突;
resize方法执行扩容
  • addEntry方法里满足扩容条件时,调用resize方法进行扩容,参数是当前table长度的2倍
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
  • 扩容时,先创建一个新的数组,然后调用transfer方法将原有的元素移动到新数组里;
  • transfer方法的第二个参数控制移动元素时,是否需要重新rehash,如果不需要rehash,则根据Entry记录的hash值,直接重新计算index即可;
transfer方法移动元素
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • 扩容时,链表里的元素依然使用头插法插入新的数组下的链表里,结果会产生扩容后链表中元素顺序颠倒;
  • 如果不触发rehash,则扩容后元素在新数组中的index位置为oldIndexoldIndex+oldTableSize

5.2 扩容时是否rehash

扩容时,根据扩容后table的容量调用initHashSeedAsNeeded方法,根据返回值确定是否需要rehash:

final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}
  • hashSeed初始值为0,所以currentAltHashingfalse
  • 并且,只有当switchtrueuseAltHashing也为true时,才会修改hashSeed;而useAltHashingtrue时,要想switch也为true,则要求hashSeed==0,所以只有在第一次开启rehash时,才会修改hashSeed
  • sun.misc.VM.isBooted()即虚拟机是否启动,为true;
  • Holder.ALTERNATIVE_HASHING_THRESHOLD是开启rehashtable容量最大阈值,即当table容量大于等于该值时,开启rehash,由内部类Hold管理;该值的默认值是ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;说明一般情况下不会开启rehash;
  • 所以currentAltHashing=false并且useAltHashing=false,异或运算结果为false,即switchingfalse
  • 结论:一般情况下不开启rehash,只有当扩容后新table的容量超过了需要开启rehash的阈值时,才会开启rehash

5.3 rehash阈值修改

HashMap的静态内部类Holder管理ALTERNATIVE_HASHING_THRESHOLD的值:

private static class Holder {

    /**
     * Table capacity above which to switch to use alternative hashing.
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD;

    static {
        String altThreshold = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction(
                "jdk.map.althashing.threshold"));

        int threshold;
        try {
            // altThreshold不为null时,使用altThreshold,否则使用ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
            threshold = (null != altThreshold)
                    ? Integer.parseInt(altThreshold)
                    : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

            // disable alternative hashing if -1
            if (threshold == -1) {
                threshold = Integer.MAX_VALUE;
            }

            if (threshold < 0) {
                throw new IllegalArgumentException("value must be positive integer.");
            }
        } catch(IllegalArgumentException failed) {
            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
        }

        ALTERNATIVE_HASHING_THRESHOLD = threshold;
    }
}
  • altThreshold可以用来修改开启rehash的阈值,该值在jdk.map.althashing.threshold里配置,如果没有配置,则使用ALTERNATIVE_HASHING_THRESHOLD_DEFAULT,见HashMap属性一节,该值默认为Integer.MAX_VALUE

二、 JDK7中的ConcurrentHashMap

  • 不支持null:JDK7中ConcurrentHashMap存放的keyvalue都不能为null,否则会抛异常;
  • 线程安全:JDK7中的ConcurrentHashMap是线程安全的,使用的是分段加锁的方式,即ConcurrentHashMap中维护一个Segment数组,Segment内部维护一个HashEntry数组,相当于将HashEntry数组切割为多段,在同一个Segment中的HashEntry数组使用同一把锁;
  • Segment内部类Segment类继承了ReentrantLock类,所有JDK7中的ConcurrentHashMap使用的是ReentrantLock锁;
  • 分段扩容:添加元素触发扩容时只扩容对应Segment对象中的HashEntry数组,不影响其他对象中的数组(除非扩容的是Segment数组中的第0号元素,因为该元素作为创建其他Segment元素的模板);
  • Segment数组长度不变ConcurrentHashMap初始化后,Segment数组的长度不再变化;
  • Segment数组长度ConcurrentHashMapSegment数组的长度为大于等于concurrentLevel且值为2的幂次方的最小值,默认值为16
  • HashEntry数组长度Segment对象中HashEntry数组的长度为initialCapacity除以Segment数组的商向上取整,然后找到大于等于该商且值为2的幂次方的最小值,如果该值小于2,则默认取值为2,默认值是2

1. Hashtable存在的问题

  • Hashtable是从JDK1.0加入的并发安全的Map;
  • Hashtable直接在所有的方法上加synchronized的关键字,将整个方法变成同步方法,虽然线程安全,但是效率不高;
  • ConcurrentHashMap替代了Hashtable实现一个更高效的线程安全的Map;

2. 属性字段

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    
    /* ---------------- Constants -------------- */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的 concurrentLevel
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // 每个分段数组的最小容量,必须是2的幂次方
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    // 最大分段量,必须是2的幂次方,且小于2的24次方
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    // 获取锁的重试次数
    static final int RETRIES_BEFORE_LOCK = 2;

    /* ---------------- Fields -------------- */
    private transient final int hashSeed = randomHashSeed(this);

    // 根据index计算分段值的掩码
    final int segmentMask;

    final int segmentShift;

    // 分段,每个分段都是一部分数组
    final Segment<K,V>[] segments;

    transient Set<K> keySet;
    transient Set<Map.Entry<K,V>> entrySet;
    transient Collection<V> values;
    
    // 省略方法
    // ...
}
  • ConcurrentHashMap内部维护了一个Segment数组;

Segment内部类

// 内部类Segment
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

        transient volatile HashEntry<K,V>[] table;

        transient int count;

        transient int modCount;

        transient int threshold;

        final float loadFactor;
        
        // 省略方法
        // ...
    }
  • Segment对象内部维护了loadFactorthresholdmodCount等属性和一个HashEntry数组;

HashEntry内部类

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
    
    // 省略方法
    // ...
}

3. 初始化

3.1 构造方法初始化

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    
    /**
     * #1 concurrentLevle最大值 1<<16 即,2的16次方
     */
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    
    /**
     * #2 ssize为大于等于concurrentLevel且为2的幂次方的最小值
     */
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    
    
    /**
     * #3 initialCapacity最大值为1<<30 即,2的30次方
     */
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    
    /**
     * #4 c等于初始化容量除以大于等于concurrentLevel且为2的幂次方的最小值;如果除法结果有余数,则c再加1;
     * ssize是2的幂次方,initialCapacity不一定是2的幂次方
     */
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
        
    /**
     * #5 cap初始化为segment中table的最小容量,即2
     */
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    
    /**
     * #6 如果cap小于c,则计算为大于等于c且值为2的幂次方的最小值
     */
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    
    /**
     * #7 创建 Segment对象,Segment对象中HashEntry数组元素个数为cap个
     */
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    
    /**
     * #8 创建Segment数组,数组元素个数为ssize
     */
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}
默认初始化参数执行过程

默认初始化参数,initialCapacity16concurrencyLevel也为16:

  • #2结束: ssize=16,设置ssize为大于等于concurrencyLevel且为2的幂次方的最小值;
  • #4结束:c=1ssize=16,设置cinitialCapacity除以ssize的商值,如果有余数再加1;
  • #5结束:cap=2c=1ssize=16
  • #6结束:cap=2c=1ssize=16,设置cap为大于等于c且值为2的幂次方的最小值,且不小于2;
  • #7结束:Segment对象中HashEntry数组长度为cap=2
  • #8结束:ConcurremtHashMap中Segment数组长度为ssize=16;
自定义初始化参数执行过程

调用构造函数,传入初始化参数,initialCapacity50concurrencyLevel也为10:

  • #2结束: ssize=16
  • #4结束:c=4ssize=16
  • #5结束:cap=2c=4ssize=16
  • #6结束:cap=4c=4ssize=16
  • #7结束:Segment对象中HashEntry数组长度为cap=4
  • #8结束:ConcurremtHashMap中Segment数组长度为ssize=16;

总结:

  • ConcurrentHashMap中Segment数组的长度为:大于等于concurrentLevel且值为2的幂次方的最小值;默认为16
  • Segment对象中HashEntry数组的长度为:大于等于( initialCapacity除以ConcurrentHashMapSegment数组长度的商向上取整)且值为2的幂次方的最小值,且不小于2;初始默认值为2;
  • 所以ConcurrentHashMap如果使用默认构造方法,则初始化后ConcurrentHashMap中Segment数组有16个元素,每个Segment元素中HashEntry数组有2个元素,共有16*2个HashEntry元素;
  • 初始化一个Segment0对象的作用是:记录计算出来的Segment对象相关的参数,在创建其他Segment对象时,直接使用0号位置的Segment对象属性即可;
构造方法初始化的数据

构造方法执行了以下操作:

  • 确定了ConcurrentHashMapSegment数组的大小;
  • 确定了Segment对象中HashEntry数组的大小;
  • 初始化ConcurrentHashMap的Segment数组;
  • 初始化一个Segment对象放到Segment数组的0号位置;

4. 添加元素

  • 添加元素也是使用的头插法;

4.1 添加元素步骤

  1. 根据key值计算hash值;
  2. 根据hash值计算元素在Segment数组的下标j
  3. 根据下标j找到Segment对象,如果不存在则创建;
  4. 调用Segment对象的put方法添加元素;
  5. Segment的put方法根据hash值计算出keyHashEntry数组的下标;
ConcurrentHashMap的put方法
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
  • value==null时,会抛出异常,说明ConcurrentHashMap不能存放value=null的节点;
  • (hash >>> segmentShift) & segmentMask的作用是:取hash值的高位,计算节点在Sgement数组的下标j
  • 计算元素在Segment数组中的位置使用hash高位,而计算元素在HashEntry数组中的位置使用hash低位,是为了提高散列性,如果计算两个数组下标使用的是相同的高位或低位,则进入Segment对象的hash值计算的HashEntry的下标大概率会相同,从而导致Segment对象中HashEntry数组不散列;
  • Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)的作用是从数组segments里取出下标为j的元素;
  • s = ensureSegment(j);的作用是,如果segments数组中下标为j的元素不存在,则创建一个Segment对象;
  • 最后调用Segment对象的put方法将元素添加到Segment中的HashEentry数组中;
生成Segment对象的ensureSegment方法
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}
  • (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)的作用是从ss数组里获取下标为k(下标不是uu是根据k计算出来的偏移量)的Segment对象;
  • 方法中判断了三次(seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null,最后使用UNSAFEcompareAndSwapObject操作,说明创建Segment处理多线程并发不是使用锁,而是使用自旋锁while循环)+CAS操作(比较与交换,乐观锁)
  • 创建Segment对象时,根据ss[0]对象作为模板,这里解释了为什么在构造方法初始化时会初始化一个ss[0]对象;
Segment的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                
                // #1 扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}
  • Segment类继承在ReentrantLock,在添加元素之前先tryLock加锁,添加完成后unlock解锁;
  • tryLock尝试获取锁失败后不会阻塞,会立即返回false
  • lock方法获取锁失败后会阻塞等待;
  • #1位置判断是否需要扩容的条件是(c > threshold && tab.length < MAXIMUM_CAPACITY)
scanAndLockForPut方法重新尝试获取锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}
  • 如果put方法中第一次尝试获取锁tryLock失败,则使用scanAndLockForPut方法重试tryLock,并且在重试过程提取创建Node节点;
  • 如果尝试次数达到了最大次数,则直接调用lock方法,阻塞等待锁;
  • lock方法获取锁失败会线程阻塞,不会占用cpu资源,而如果在while循环里不断使用tryLock方法尝试获取锁,则会一直占用cpu资源(自旋锁)

5. 扩容

每一个Segment对象是一个独立的存放HashEntry的Map,扩容时是扩容Segment对象里的HashEntry数组;

扩容条件

Segment的put方法里判断是否需要扩容,条件是(c > threshold && tab.length < MAXIMUM_CAPACITY),即添加元素后Segmenttable数组长度大于扩容阈值thresholdtable长度小于最大长度时执行扩容;

扩容方法 rehash
private void rehash(HashEntry<K,V> node) {
    
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            
            /**
             * #1 如果链表中只有一个元素,则直接将元素移动到新数组
             */
            if (next == null)   //  Single node on list
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                
                /**
                 * #2 将链表最后新下标相同的节点直接移动到新数组
                 */
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                
                /**
                 * #3 将除尾部的元素克隆到新数组
                 */
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}
  • 移动与克隆
    • #1位置的代码: 如果数组链表中只有一个元素,则直接移动到新数组;
    • #2位置的for循环:将链表尾部连续的新下标相同的元素移动到新数组;
    • #3位置的for循环:将剩余的元素(如果还有的话)克隆到新数组中;
  • ConcurrentHashMap扩容时,没有重新计算hash值,也没有控制重新计算hash值的参数开关;
  • 扩容只是将对应的Segment对象中的HashEntry数组的容量扩容一倍,如果扩容的不是ss[0]元素(ss[0]位置的Segment对象是创建新对象的模板),则不影响其他的Segment对象;
  • rehash方法本身不需要加锁,因为外层调用put方法已经加锁了;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值