JAVA并发编程:深入解析JDK1.7中HashMap、并发容器ConcurrentHashMap原理及其源码分析

一、HashMap详解

1、基本介绍

1.1 概述

  HashMap是基于Map接口实现的,元素以key-value的方式存储在map在中,此实现提供所有可选的映射操作,并允许null的key和null的value。 HashMap类与Hashtable类大致等效,不同之处在于它不是线程安全的,并且允许为null。HashMap的UML图如下图所示:
在这里插入图片描述

1.2 构造方法

  HashMap一共有4个构造方法,如下图所示:
在这里插入图片描述

 /**
 *  构造一个空的HashMap,默认容量为16,负载因子为0.75
 */
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
 *  构造一个空的HashMap,容量为指定的initialCapacity,负载因子默认为0.75
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
 *  构造一个空的HashMap,具有指定初始值容量和负载因子的HashMap
 */
 public HashMap(int initialCapacity, float loadFactor) {
     // 判断传入的初始容量是否大于0,如果小于0,则抛出相应异常信息。
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
     // 判断传入的初始容量是否大于最大容量,如果大于最大容量,则将initialCapacity赋值为最大容量MAXIMUM_CAPACITY,1 << 30,1左移30位
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
     // 如果负载因子loadFactor小于0 或者 如果指定的数字不是一个数字(NaN),返回{true,否则返回false
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);

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

1.3 基本属性

常量:

  • static final int DEFAULT_INITIAL_CAPACITY:默认初始容量,1 << 4,1左移4位,默认大小为16。
  • static final int MAXIMUM_CAPACITY:最大容量,如果任何一个带参数的构造函数隐式指定了较大的值,则使用。
  • static final float DEFAULT_LOAD_FACTOR:默认负载因子,值为0.75f。

成员变量:

  • transient int size:map中存储key-value的个数
  • transient Entry<K,V>[] table:entry数组,长度必须是2的幂。
  • int threshold:扩容的阈值(capacity * load factor),如果table == EMPTY_TABLE,那么这个值为map初始容量大小。
  • final float loadFactor:哈希表中的负载因子

2、数据结构

  HashMap由数组和链表来实现对数据的存储。HashMap采用Entry数组来存储key-value键值对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
  HashMap中实现了一个静态内部类Entry,其重要的属性有 hash,key,value,next。如下图所示:
在这里插入图片描述
JDK1.7数据结构如下图所示:
在这里插入图片描述

3、源码解析

3.1 put过程分析

  put(K key, V value)方法

public V put(K key, V value) {
   // 判断table数组是否为空,如果为空,则初始化数组
   if (table == EMPTY_TABLE) {
       inflateTable(threshold);
   }
   // 如果key为空,调用putForNullKey方法put数据,最终会将元素存储在table[0]中
   if (key == null)
       return putForNullKey(value);
   // 计算key的哈希值
   int hash = hash(key);
   // 根据哈希值和table数组长度计算数据存储在table中的索引下标
   int i = indexFor(hash, table.length);
   // 取出table数组中索引i位置处的Entry e,如果e不为空,循环遍历Entry链表,判断是否有重复的key存在,如果有替换掉key的值
   for (Entry<K,V> e = table[i]; e != null; e = e.next) {
       Object k;
       // 如果当前节点e的哈希值、key与要put元素的哈希值、key相等
       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
           V oldValue = e.value;
           // 使用新值替换掉旧值
           e.value = value;
           // 每当对HashMap中已经存在的键k的put(k,v)调用覆盖条目中的值时,就会调用recordAccess方法。
           e.recordAccess(this);
           // 返回旧值
           return oldValue;
       }
   }

   // 记录修改次数,结构修改是指那些改变HashMap中映射数量或修改其内部结构的修改(例如,重新哈希)。
   modCount++;
   // 如果不存在重复的key,将当前元素添加到Entry链表中,后面会详细介绍此逻辑
   addEntry(hash, key, value, i);
   return null;
}

  put方法中的初始化数组 inflateTable(int toSize)方法

// put方法中的初始化数组 inflateTable(int toSize)方法
private void inflateTable(int toSize) {
    // roundUpToPowerOf2方法的目的就是根据传入toSize计算出一个合理的初始容量,保证数组大小始终为2的n次方。
    // 比如在new HashMap(17),通过此方法计算后得出初始化数组的大小为32,也就是最终的capacity要大于等于离toSize最近的2的n次方的数
    int capacity = roundUpToPowerOf2(toSize);

    // 计算扩容的阈值:capacity * loadFactor
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //  new一个初始数组,大小为capacity
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

  putForNullKey(V value) 方法:当元素的key为null时,调用putForNullKey(V value) 方法存储元素数据,实际上当key为null时,数据是存储在table[0]的位置

private V putForNullKey(V value) {
    // 获取table[0]位置的数据,如果e不为null,遍历循环链表
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        // 如果e元素的key为null,使用新值替换掉旧值,并返回旧值。
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 如果table[0]位置没有元素的key为null,则将元素添加到table[0]的链表中
    addEntry(0, null, value, 0);
    return null;
}

  将元素添加到Entry链表中,调用addEntry(int hash, K key, V value, int bucketIndex)方法。将具有指定键、值和散列码的元素添加到指定bucket位置。此方法负责在适当的情况下对table数组进行扩容处理

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果map中的元素个数size >= 扩容阈值threshold,并且在table的bucketIndex索引下标处有值,需要进一步扩容操作
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容操作,数组大小扩容为原来数组大小的2倍。
        resize(2 * table.length);
        // 扩容以后重新计算hash值,如果key为null,hash值为0。
        hash = (null != key) ? hash(key) : 0;
        // 扩容后重新计算下标
        bucketIndex = indexFor(hash, table.length);
    }

    // 根据元素hash、key、value值创建一个Entry,赋值到table的bucketIndex位置
    createEntry(hash, key, value, bucketIndex);
}
// 将新值放到链表的表头,size++
void createEntry(int hash, K key, V value, int bucketIndex) {
    // 获取table数组中bucketIndex位置的值
    Entry<K,V> e = table[bucketIndex];
    // new 一个 Entry ,将新加入的元素放在链表的表头
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // put中元素个数加1
    size++;
}

  扩容过程:将此map中的数据重新散列到具有更大容量的新数组中。当此map中的键数达到其阈值时,将自动调用此方法。如果当前容量是MAXIMUM_CAPACITY,此方法不会调整map的大小,而是将阈值设置为Integer.MAX_VALUE。这具有防止以后调用的作用。

void resize(int newCapacity) {
    // 旧数组entry
    Entry[] oldTable = table;
    // 旧数组大小
    int oldCapacity = oldTable.length;
    // 如果旧数组大小等于最大容量值,则将扩容的阈值设置为Integer.MAX_VALUE,作用是防止以后再调用。
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 创建一个新的table
    Entry[] newTable = new Entry[newCapacity];
    // 将所有entry从当前table转移到newTable
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 重新将table数据赋值为newTable
    table = newTable;
    // 计算下一次扩容的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

   将所有entry从当前table转移到newTable。

void transfer(Entry[] newTable, boolean rehash) {
    // 获取新table容量大小
    int newCapacity = newTable.length;
    // 循环遍历旧的table数据
    for (Entry<K,V> e : table) {
        // 当entry不为null
        while(null != e) {
            // 获取链表entry的下一个元素
            Entry<K,V> next = e.next;
            // 如果需要重新计算哈希值
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新计算下标
            int i = indexFor(e.hash, newCapacity);
            // 当前entry元素的下一个元素为新的table数组中的值(也就是头插法)
            e.next = newTable[i];
            // 将entry值赋值到新table的索引i位置
            newTable[i] = e;
            // 保证旧table的entry在下一次while循环使用
            e = next;
        }
    }
}

3.2 get过程分析

  get过程相对put过程来说简单得很多,首先根据key计算hash值,再通过位运算(h & (length-1))找到数组索引下标,最后遍历数组下标位置处的Entry链表,找到对应的值即返回。返回指定key的值,如果map中不包含指定key的值,则返回null。

public V get(Object key) {
    // 如果key为null,调用getForNullKey()方法,从table的0下标处取值
    if (key == null)
        return getForNullKey();
    // 返回HashMap中指定的key相关联的entry。如果HashMap不包含key的映射,则返回null。
    Entry<K,V> entry = getEntry(key);
    // 返回具体指
    return null == entry ? null : entry.getValue();
}

  getEntry(Object key)方法

// 返回指定key的值,如果map中不包含指定key的值,则返回null。
final Entry<K,V> getEntry(Object key) {
    // 如果map中没有数据,则返回null。
    if (size == 0) {
        return null;
    }

    // 计算hash值
    int hash = (key == null) ? 0 : hash(key);
    // 获取索引位置处的Entry,循环遍历Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        // 如果entry的hash值与传入的key对应的hash值相等,且key也相等,则返回entry
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    // 未找到对应数据,返回null。
    return null;
}

3.3 remove过程分析

  remove过程也是非常多简单,大概思路就是根据指定的key计算出hash值和table的索引值,循环遍历链表找到对应的entry删除即可。

//删除并返回与HashMap中指定key相关联的entry。如果HashMap不包含此key的map,则返回null。
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
    // 如果map中没有数据,则返回null。
    if (size == 0) {
        return null;
    }
    // 计算hash值
    int hash = (key == null) ? 0 : hash(key);
    // 计算数组索引下标
    int i = indexFor(hash, table.length);
    // 获取索引处的entry值,前驱entry
    Entry<K,V> prev = table[i];
    // 当前entry
    Entry<K,V> e = prev;

    while (e != null) {
        // 当前entry的下一个entry
        Entry<K,V> next = e.next;
        Object k;
        // 如果当前entry的hash等于需要删除key对应的hash值,且key不为null,相等
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            // 记录修改次数
            modCount++;
            // map中元素个数-1
            size--;
            // 删除的是链表中第一个entry,断开当前entry e,table中i索引处的赋值为next
            if (prev == e)
                table[i] = next;
            else
                prev.next = next; // 断开当前entry e,前驱entry的下一个entry为next
            e.recordRemoval(this);
            // 返回删除的entry信息
            return e;
        }
        // 未找到删除的元素,重新给前驱prev赋值为e,当前e赋值为next
        prev = e;
        e = next;
    }

    return e;
}

二、ConcurrentHashMap详解

1、基本介绍

1.1 概述

  在Java1.5中,并发编程大师Doug Lea给我们带来了concurrent包,而该包中提供的ConcurrentHashMap是线程安全并且高效的HashMap。在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下。HashMap 之所以在并发下的扩容造成死循环,是因为多个线程并发进行时,因为一个线程先期完成了扩容,将原 Map 的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表,当 get 表中不存在的元素时,造成死循环。于是在多线程并发处理下,ConcurrentHashMap解决了HashMap在扩容到时候造成链表形成环形结构的问题。ConcurrentHashMapUML图如下图所示:
在这里插入图片描述

1.2 构造方法

  ConcurrentHashMap一共有5个构造方法,如下图所示:
在这里插入图片描述
  ConcurrentHashMap 初始化方法是通过initialCapacity、loadFactor 和concurrencyLevel(参数concurrencyLevel 是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个map,根据这个来确定Segment 数组的大小concurrencyLevel 默认是DEFAULT_CONCURRENCY_LEVEL = 16;)等几个参数来初始化segment 数组、段偏移量segmentShift、段掩码segmentMask 和每个segment 里的HashEntry 数组来实现的。
  并发级别可以理解为程序运行时能够同时更新 ConccurentHashMap 且不产 生锁竞争的最大线程数,实际上就是 ConcurrentHashMap 中的分段锁个数,即 Segment[]的数组长度。ConcurrentHashMap 默认的并发度为 16,但用户也可以 在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap 会使用大 于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际 并发度则为 32)。
  如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大, 原本位于同一个 Segment 内的访问会扩散到不同的 Segment 中,CPU cache 命中 率会下降,从而引起程序性能下降。
  segments 数组的长度 ssize 是通过 concurrencyLevel 计算得出的。为了能通 过按位与的散列算法来定位 segments 数组的索引,必须保证 segments 数组的长 度是 2 的 N 次方(power-of-two size),所以必须计算出一个大于或等于 concurrencyLevel 的最小的 2 的 N 次方值来作为 segments 数组的长度。假如 concurrencyLevel 等于 14、15 或 16,ssize 都会等于 16,即容器里锁的个数也是 16。

/**
 * 创建一个空的map,默认初始容量(16)、负载因子(0.75)和concurrencyLevel(16)。
 */
public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
 * 创建一个空的map,指定初始容量,且默认负载因子(0.75)和concurrencyLevel(16)。
 */
public ConcurrentHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
 * 创建一个空的map,指定初始容量和负载因子,且默认concurrencyLevel(16)。
 */
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
/**
 * 创建一个空的map,指定初始容量、负载因子和concurrencyLevel。
 */
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
    // 验证参数合法性
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 如果并发级别concurrencyLevel大于允许的最大值 MAX_SEGMENTS = 1 << 16,则concurrencyLevel等于最大值
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    // segment数组的大小,根据并发级别来计算的。
    // 以必须计算出一个大于或等 concurrencyLevel 的最小的2 的N 次方值来作为segments 数组的长度。
    // 假如concurrencyLevel 等于14、15 或16,ssize 都会等于16,即容器里锁的个数也是16。
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 如果使用默认的concurrencyLevel 16,那么计算出来的ssize为16,sshift为4,segmentShift为28,segmentMask为15
    // 段偏移量
    this.segmentShift = 32 - sshift;
    // 段掩码
    this.segmentMask = ssize - 1;
    // 如果指定的初始容量大于最大容量,则initialCapacity为最大容量值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    // segment 里HashEntry 数组的长度,默认最小值为2,因为这样的话,对于具体的segment,在插入第一个元素的时候不会扩容,插入第二个是时候才会扩容
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;

    // 创建一个segment数组,只初始化segments[0]位置处的HashEntry
    Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                    (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 将segments[0]写入数组中
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

1.3 基本属性

ConcurrentHashMap中的常量

  • static final int DEFAULT_INITIAL_CAPACITY:table的默认初始容量,在构造函数中未指定时使用,默认大小为16。
  • static final float DEFAULT_LOAD_FACTOR:table的默认负载因子,在构造函数中未指定时使用,默认大小为0.75f。
  • static final int DEFAULT_CONCURRENCY_LEVEL:table的默认并发级别,在构造函数中未指定时使用,默认大小为16。
  • static final int MAXIMUM_CAPACITY:table的最大容量,如果任何一个带参数的构造函数指定了较大的值,则使用。大小为1 << 30,也就是1*2的30次方。
  • static final int MIN_SEGMENT_TABLE_CAPACITY:每个segment中HashEntry 的table 最小容量。必须是2的n次方,默认为2。
  • static final int MAX_SEGMENTS:最大segment容量,值为1 << 16。

ConcurrentHashMap中的成员变量

  • final Segment<K,V>[] segments:segment,每个segment都是一个专用的hash table。

Segment中的常量

  • static final int MAX_SCAN_RETRIES:单核CPU的值为1,多核CPU的值为16。

Segment中的成员变量

  • transient volatile HashEntry<K,V>[] table:每个segment中的table
  • transient int count:元素个数
  • transient int modCount:segment中mutative operations总数
  • transient int threshold:扩容的阈值
  • final float loadFactor:负载因子

Segment内的构造函数:

Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
    this.loadFactor = lf;
    this.threshold = threshold;
    this.table = tab;
}

2、数据结构

  ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据 进行修改时,必须首先获得与它对应的 Segment 锁。如下图所示:
在这里插入图片描述

JDK1.7数据结构如下图所示:
在这里插入图片描述

3、源码解析

3.1 put过程源码解析

  put(K key, V value)方法:将指定的key-value存储到map中,且key-value都不能为空。可以通过调用get方法来获取相应值。首先我们来看一下ConcurrentHashMap中的put方法,在此方法中主要是确定新值要插入到哪一个segment中,而真正put数据的操作是在Segment类中的put方法。

public V put(K key, V value) {
    Segment<K,V> s;
    // 判断参数是否为空
    if (value == null)
        throw new NullPointerException();
    // 计算key的hash值,如果key为空,则报空指针异常
    int hash = hash(key);
    // 根据 hash 值找到 Segment 数组中的位置 j
    // hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位
    // 再和segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是segment的数组下标
    int j = (hash >>> segmentShift) & segmentMask;
    // 在初始化的时候只初始化了segment[0],但是其他位置还是 null,ensureSegment(j) 对 segment[j] 进行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 插入新值到segment中
    return s.put(key, hash, value, false);
}

   segment中的put方法,由数组+链表组成,这个HashMap数据结构一样。

 // segment中的put方法,由数组+链表组成,这个HashMap数据结构一样。
 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
     // 在往该 segment 写入前,需要先获取该 segment 的独占锁
     // put 方法会通过tryLock()方法尝试获得锁,获得了锁,node 为null 进入try语句块,
     // 没有获得锁,调用scanAndLockForPut 方法自旋等待获得锁。
     HashEntry<K,V> node = tryLock() ? null :
         scanAndLockForPut(key, hash, value);
     V oldValue;
     try {
         // segment中的HashEntry数组table
         HashEntry<K,V>[] tab = table;
         // 根据hash值和tab长度计算索引下标
         int index = (tab.length - 1) & hash;
         // 获取索引index处的HashEntry值,first 是tab了数组该位置处的链表的表头
         HashEntry<K,V> first = entryAt(tab, index);
         // for循环遍历first,两种情况,一种是index处有元素,另一种是index处没有元素。
         for (HashEntry<K,V> e = first;;) {
             // 如果e不为空
             if (e != null) {
                 K k;
                 // 如果HashEntry中的key等于新值key或者HashEntry中的hash等于新值hash,且key相等
                 if ((k = e.key) == key ||
                     (e.hash == hash && key.equals(k))) {
                     oldValue = e.value;
                     // 判断是否替换旧值
                     if (!onlyIfAbsent) {
                         e.value = value;
                         ++modCount;
                     }
                     break;
                 }
                 // 继续执行链表下一个HashEntry
                 e = e.next;
             }
             else {
                // node不为 null,那就直接将它设置为链表表头
                 if (node != null)
                     node.setNext(first);
                 else
                     // node为null,初始化并设置为链表表头
                     node = new HashEntry<K,V>(hash, key, value, first);
                 int c = count + 1;
                 // 当table数组的大小超过阈值时,将对其进行重新散列、扩容操作。
                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                     rehash(node);
                 else
                      // 没有达到阈值,将 node 放到数组 tab 的 index 位置,也就是将新的节点设置成原链表的表头
                     setEntryAt(tab, index, node);
                 ++modCount;
                 count = c;
                 oldValue = null;
                 break;
             }
         }
     } finally {
         // 释放锁
         unlock();
     }
     return oldValue;
 }

  ensureSegment(int k)方法:ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽,在插入第一个值的时候再进行初始化。 ensureSegment 方法考虑了并发情况,多个线程同时进入初始化同一个槽 segment[k],但只要有一个成功就可以了。

// 初始化segment:返回给定索引的segment,创建它并(通过CAS)记录在segmetn table 中(如果还没有)。
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[0] 了,
        // 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
        // 为什么要用“当前”,因为 segment[0] 可能早就扩容过了
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype,使用 segment[0]作为原型,这也就是为什么要初始化segment[0]的原因
        int cap = proto.table.length; // segment[0]中table数组的长度
        float lf = proto.loadFactor;  // segment[0]中负载因子
        int threshold = (int)(cap * lf); // 计算下次扩容的阈值
        // 创建一个HashEntry数组,容量为cap,也就是segment[k]内部的数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck,再次检查一遍该segment[k]是否被其他线程初始化了。
            // 创建一个segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // while 循环 CAS操作,保证多线程下只有一个线程可以成功
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                 // 使用CAS操作,当前线程成功设值或其他线程成功设值后,退出
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

  scanAndLockForPut(K key, int hash, V value)方法:scanAndLockForPut 方法里在尝试获得锁的过程中会对对应hashcode 的链表进行遍历,如果遍历完毕仍然找不到与key 相同的HashEntry 节点,则为后续的 put 操作提前创建一个HashEntry。当tryLock 到一定次数后仍无法获得锁,则进入到阻塞队列等待锁,通过lock申请锁。

 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
                     //  创建一个HashEntry
                     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) {
            // 如果重试次数大于最大的MAX_SCAN_RETRIES(单核是1,多核是64),则进入到阻塞队列等待锁,改为lock()方式获取锁,lock()是阻塞方法,直到获取锁后返回
             lock();
             break;
         }
         else if ((retries & 1) == 0 &&
                  (f = entryForHash(this, hash)) != first) {
             // 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
              // 所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
             e = first = f; // re-traverse if entry changed
             retries = -1;
         }
     }
     return node;
 }

  rehash(HashEntry<K,V> node)方法:扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性 table。为了避免让所有的节点都进行复制操作:由于扩容是基于 2 的幂指来操作, 假设扩容前某 HashEntry 对应到 Segment 中数组的 index 为 i,数组的容量为 capacity,那么扩容后该 HashEntry 对应到新数组中的 index 只可能为 i 或者 i+capacity,因此很多 HashEntry 节点在扩容前后 index 可以保持不变。

// 对table数组进行扩容处理
private void rehash(HashEntry<K,V> node) {
   // 旧table数组的数据
   HashEntry<K,V>[] oldTable = table;
   // 旧table数组大小
   int oldCapacity = oldTable.length;
   // 新的table数组大小,为原来table数组大小的2倍
   int newCapacity = oldCapacity << 1;
   // 计算下次扩容的阈值
   threshold = (int)(newCapacity * loadFactor);
   // 创建一个新的HashEntry数组
   HashEntry<K,V>[] newTable =
           (HashEntry<K,V>[]) new HashEntry[newCapacity];
   //新的掩码,用于计算元素下标位置
   int sizeMask = newCapacity - 1;
   // 遍历旧数组,将原来i位置处的元素存储到新数组的i或者i+oldCapacity位置处
   for (int i = 0; i < oldCapacity ; i++) {
       // 获取i位置处的元素数据e
       HashEntry<K,V> e = oldTable[i];
       if (e != null) { // 数据不为空
           // 当前元素e的下一个元素next
           HashEntry<K,V> next = e.next;
           // 计算当前元素e在新数组中的位置idx
           int idx = e.hash & sizeMask;
           // 如果next为空,说明i位置处只有一个元素,那么久简单了,直接赋值给newTable[idx]
           if (next == null)   //  Single node on list(单节点列表)
               newTable[idx] = e;
           else { // Reuse consecutive sequence at same slot
               // e表示链表头结点
               HashEntry<K,V> lastRun = e;
               // idx表示当前元素e的位置
               int lastIdx = idx;
               // 遍历这个for循环,找到一个lastRun节点,这个节点之后的所有元素放在table同一个下标位置
               for (HashEntry<K,V> last = next;
                    last != null;
                    last = last.next) {
                   // 重新计算last节点的下标位置
                   int k = last.hash & sizeMask;
                   if (k != lastIdx) {
                       lastIdx = k;
                       lastRun = last;
                   }
               }
               //将lastRun节点及其之后的所有节点组成的链表放到新table的lastIdx这个位置
               newTable[lastIdx] = lastRun;
               // Clone remaining nodes,克隆剩下的节点
               //处理lastRun之前的节点信息,这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
               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);
               }
           }
       }
   }
   // 将新的node节点放到newTable中nodeIndex位置的头部
   int nodeIndex = node.hash & sizeMask; // add the new node
           node.setNext(newTable[nodeIndex]);
   newTable[nodeIndex] = node; // 将node赋值给newTable[nodeIndex]完成节点的插入
   table = newTable;
}

3.2 get过程分析

  get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment(使用了散列值的高位部分),再通过散列算法定位到 table(使用了散列值 的全部)。整个 get 过程,没有加锁,而是通过 volatile 保证 get 总是可以拿到最 新值。

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    // 计算key的hash值
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 找到对应的segment,在segment数组中找到对应的table
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
        // 遍历table下指定HashEntry链表
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

3.3 remove过程分析

  与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。从map中删除指定key,如果map中不存key,则不做任何操作。
ConcurrentHashMap中的remove方法

public V remove(Object key) {
	// 计算hash值
    int hash = hash(key);
    // 根据hash值获取对应的segment
    Segment<K,V> s = segmentForHash(hash);
    // 调用segment中的remove方法
    return s == null ? null : s.remove(key, hash, null);
}

Segment中的remove方法

final V remove(Object key, int hash, Object value) {
    // 如果尝试获取锁失败,调用scanAndLock方法获取锁
    if (!tryLock())
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        HashEntry<K,V>[] tab = table;
        // 计算key在table数组中的索引下标。
        int index = (tab.length - 1) & hash;
        // 获取index位置处的元素值
        HashEntry<K,V> e = entryAt(tab, index);
        HashEntry<K,V> pred = null; // e元素的前节点
        // 循环index位置处的HashEntry链表
        while (e != null) {
            K k;
            // 元素e的下一个元素
            HashEntry<K,V> next = e.next;
            // 如果元素e的key与传入参数key相等或者元素e的hash值、key等于传入参数的hash值、key,表明已经找到了remove的元素
            if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                // 获取元素e的value值
                V v = e.value;
                if (value == null || value == v || value.equals(v)) {
                    if (pred == null) // 表示remove的是第一个元素,直接将next插入到index位置即可
                        setEntryAt(tab, index, next);
                    else
                        pred.setNext(next); // 断开连接,直接将e元素的前节点的next节点设置为next
                    ++modCount; 
                    --count; // map中的元素个数减1
                    oldValue = v;
                }
                break;
            }
            // 下一次循环元素e的前节点元素
            pred = e;
            // 下一次循环的元素e
            e = next;
        }
    } finally {
        // 释放锁
        unlock();
    }
    return oldValue;
}

  在ConcurrentHashMap还有一个remove方法,根据指定的key和value来remove元素,逻辑跟单独指定key的差不多,在此不再赘述。

  本文主要讲解了JDK1.7中的HashMap和ConcurrentHashMap的基本介绍及其put、get、remove过程的源码分析,作者能力有限,难免会在某些地方分析得不到位,如有不正确的地方希望各位读者指正,大家一起进步,希望对各位有所帮助。下一篇将会详细讲解JDK1.8中的HashMap和ConcurrentHashMap,大家拭目以待。


备注:博主微信公众号,不定期更新文章,欢迎扫码关注。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值