JDK7 ConcurrentHashMap源码解析

目录

一、JDK7中的实现方式

重要参数

构造函数

put函数分析

扩容

get方法实现


一、JDK7中的实现方式

为了提高并发,在JDK7中,一个HashMap被拆分为多个子HashMap。每一个HashMap称作一个Segment,多个线程操作多个Segment互相独立。

具体来说,每个Segment都继承自ReentrantLock,Segment的数量等于锁的数量,这些锁彼此之间相互独立,即所谓的分段锁。

重要参数

// 初始化默认值 16
static final int DEFAULT_INITIAL_CAPACITY = 16; 
// 默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 该表的默认并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 数组最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 每段表的最小容量(每个HashEntry数组的长度)。必须是2的幂,至少是2,以避免在延迟构建之后在下次使用时立即调整大小。
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 允许的最大段数;用于绑定构造函数参数。必须是小于1<<24的二次幂。
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
// 用于锁定之前大小和包含值方法的非同步重试次数
static final int RETRIES_BEFORE_LOCK = 2;

构造函数

// 传入Segment[]数组的容量为16,默认加载因子0.75,默认级别16
public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
// 自定义传入Segment[]数组的容量,默认加载因子0.75,默认级别16
public ConcurrentHashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
// 自定义传入Segment[]数组的容量、加载因子,默认级别16
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
// 泛型上限,Map对象传入键值对,键-传入K类或K的子类,值-传入V类或V的子类
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY),
         DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    putAll(m);
}
// 传入Segment[]数组的容量、加载因子、默认级别,初始化
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    // 判断传入参数是否正确,否则抛非法参数异常
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 限制数组的最大并发级别
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    // 找到Segment数组长度,大于等于并发级别的2的幂次值,如:level=16,ssize=16;level=17,ssize=32
    // 这里的concurrencyLevel默认为16,所以此循环算出来sshift为4
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // segmentShift用于定位参与散列预算的位数,默认为28
    // 这里的sshift,因为上面位操作共移动了4次,2^4=16可以快速计算当前Segment数组容量,所以后面的put中计算j的值(将要存放的Segment数组中的下标) >>>(32-sshift)&15 的结果会在[0-15]区间上(其实就是让高4位参与计算)
    // 如果concurrencyLevel=17,ssize=32,sshift=5,就让高5位参与运算
    this.segmentShift = 32 - sshift;
    // 散列运算的掩码,默认为15,掩码的二进制各个位的值都是1,目的是为了计算得到key的hash值在Segment数组中的位置
    this.segmentMask = ssize - 1;
    // 限制数组容量最大数
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // Segment里HashEntry数组的长度,如果c大于1,就会取大于等于c的2的N次幂值,所以cap不是1就是2的N次方。
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    // HashEntry数组的最小长度为2
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    // 计算出segments[0]原型,下次put时,如果位置为空,则可以直接引用segments[0]中的参数,如:loadFactor、threshold 、HashEntry[]数组长度
    Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    // 创建Segment数组,默认为16大小
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 设置ss对象数组中SBASE偏移地址对应的object型field的值为s0。这是一个有序或者有延迟的方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被意外修改的时候使用才有用。
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

为了提高hash的计算性能,会保证数组的大小始终是2的整数次方。

第一个参数initialCapacity是整个concurrentHashMap的初始大小。用initialCapacity/ssize是每个Segment的初始大小。

第二个参数是负载因子,传给了Segment的内部,当每个Segment的元素个数达到一定阈值,进行rehash。Segment的个数不能扩容,但每个Segment的内部可以扩容。

put函数分析

// 这里的key和value都不能为null
public V put(K key, V value) {
    Segment<K,V> s;
    // 传入的值不能为空
    if (value == null)
        throw new NullPointerException();
    // 计算key的hash值,HashMao中key可以为null,但是这里的key不能为null;假如key可以为null的话,你无法知道get(null)返回的null是什么意思
    int hash = hash(key);
    // 在创建时已经计算出this.segmentShift = 32 - sshift; 其中sshift默认为4
    // this.segmentMask = ssize - 1; 其中ssize默认为16
    // 先对计算出的hash值右移shift位,然后在&运算mask得到当前key要存放在哪个segment[]中
    int j = (hash >>> segmentShift) & segmentMask;
    // 这里的UNSAFE.getObject取的是内存中segments数组中第j个位置的元素。并赋值给s,判断是否为空
    // (j << SSHIFT)是计算的偏移量,SBASE是segments数组的开始地址,两者相加就是将要存放的地址
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        // 如果当前位置上为空(没有Segment对象),则生成一个Segment对象,放在j个位置上,这里可能会产生并发问题
        s = ensureSegment(j);
    // 在Segment中的HashEntry[]中put数据
    return s.put(key, hash, value, false);
}

多个线程可能同时调用ensureSegment的Segment[j]进行初始化,在这个函数里需要避免重复初始化。

private Segment<K,V> ensureSegment(int k) {
    // 获取当前Segment[]对象
    final Segment<K,V>[] ss = this.segments;
    // 定位操作,跟上边 j << SSHIFT) + SBASE 一样
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    // double check,保证多个线程并发情况下,只有一个线程创建成功
    // 判断当前位置Segemnt对象是否为空
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 得到Segemnt[0]的信息,记录表长、加载因子、计算阈值,采用原型模式
        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);
        // 根据Segemnt[0]中HashEntry[],创建HashEntry[]数组,为了放入当前Segment中
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        // 这里再次判断是否已经有线程已经创建好了Segment
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
            // 创建Segment对象,并赋值
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 这里采用while循环,目的是在多线程情况下,让它一直判断是否已经创建好了Segemnt
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
                // CAS操作,保证只有一个线程创建
                // 如果还是为null,则往里面添加Segment
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    // 这里的break目的是为了结束while循环判断
                    break;
            }
        }
    }
    // 返回创建好的Segment对象
    return seg;
}

使用CAS自旋进行初始化。接下来是元素放进去的过程。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // tryLock()和lock()方法:
    //    tryLock():不会阻塞。如果能拿到锁,返回true;如果不能拿到锁,返回false。
    //    lock():会阻塞。会一直等待拿锁
    // 尝试对当前Segment对象上锁,如果能拿到锁,则返回null;如果不能拿到锁,则用scanAndLockForPut()方法去尝试获取锁的等待的过程中去执行一些其他操作。比如:去创建HashEntry对象出来
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 取当前Segment对象里面的HashEntry[]
        HashEntry<K,V>[] tab = table;
        // 计算要存放在HashEntry[]中的下标值,做的是低位&运算
        int index = (tab.length - 1) & hash;
        // 取HashEntry[]中第index中的位置,这里entryAt()方法采用的是直接取内存中的值
        HashEntry<K,V> first = entryAt(tab, index);
        // 从头开始遍历HashEntry链表
        for (HashEntry<K,V> e = first;;) {
            // 如果当前HashEntry对象不为空
            if (e != null) {
                K k;
                // 判断循环到当前的key值是否等于要存放的key值
                if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                    // 如果相等,则记录当前HashEntry的值,需要返回
                    oldValue = e.value;
                    // 这里的onlyIfAbsent默认传入为false
                    if (!onlyIfAbsent) {
                        // 更新当前的最新值,并且把hash映射的次数++
                        e.value = value;
                        ++modCount;
                    }
                    // 找到有相同的key,停止循环
                    break;
                }
                // 没有找到相同的key,则把下一个元素赋值给当前元素,继续遍历链表
                e = e.next;
            }
            // 如果当前HashEntry对象为空
            else {
                // 如果node在上边已经创建好了,则添加进去
                if (node != null)
                    node.setNext(first);
                else
                    // 创建一个HashEntry对象,也就是要插入的对象
                    node = new HashEntry<K,V>(hash, key, value, first);
                // 给HashEntry中元素+1
                int c = count + 1;
                // 如果数量超过HashEntry的阈值
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    // 则给HashEntry[]扩容
                    rehash(node);
                else
                    // 否则将当前node头插法添加到HashEntry[]的index下标中
                    setEntryAt(tab, index, node);
                // 哈希映射被修改的次数++
                ++modCount;
                // 更新当前HashEntry[]中的元素个数
                count = c;
                // 当前的key在链表中没有,已经添加到HashEntry中的情况,返回null
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解锁
        unlock();
    }
    return oldValue;
}

当put的元素存在相同的值时,判断onlyIfAbsent,如果仅不存在时插入那么就不做操作,否则进行覆盖。

如果便利到了尾部也没有相同的值,就在链表头部插入1个新节点,并把table[index]赋值为该节点。

在函数的开始,加锁的时候进行了一次优化。如果tryLock成功获得锁,如果失败执行scanAndLockForPut。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    // 通过Hash值计算Segment中相应位置上的第一个HashEntry链表元素
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    // 重试的次数
    int retries = -1; // negative while locating node
    // 循环尝试去拿锁,如果拿到锁了则返回node,没有拿到锁则进行循环
    // 也就是每次遍历链表中的节点时都会去尝试拿锁
    while (!tryLock()) {
        // 进行重新检查
        HashEntry<K,V> f; // to recheck first below
        // 刚开始为-1,则进入条件
        if (retries < 0) {
            // 一种情况是得到的链表为空,没有元素
            // 这里还有一种情况就是链表遍历到尾结点
            if (e == null) {
                // 再次判断是否有线程创建了HashEntry对象
                if (node == null) // speculatively create node
                    // 如果都满足以上条件,则创建HashEntry对象
                    node = new HashEntry<K,V>(hash, key, value, null);
                // 将重试次数置为0,不让他进入retries < 0这个条件
                // 说明此时已经创建HashEntry对象了
                retries = 0;
            }
            // 说明遍历到当前链表的key已经存在
            else if (key.equals(e.key))
                // 将重试次数置为0,不让他进入retries < 0这个条件
                // 说明此时链表中有这个HashEntry对象了
                retries = 0; 
            else
                // 继续遍历链表,向下找
                e = e.next;
        }
        // 如果当前重试次数超过最大值,则阻塞,一直等待去拿锁,拿到锁就返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        // (retries & 1) == 0 说明在retries为偶数的时候重新进行链表的判断
        else if ((retries & 1) == 0 &&
                // 这里的entryForHash()方法跟上边的一样,目的是判断通过Hash值计算Segment中相应位置上的第一个HashEntry链表元素有没有被修改(头插法的原因)。如果有改变,则说明有新元素被插入了
                 (f = entryForHash(this, hash)) != first) {
            // 将当前链表重新赋值,进行遍历
            e = first = f; // re-traverse if entry changed
            // 重新初始化retries 
            retries = -1;
        }
    }
    // 返回拿到锁的node节点
    return node;
}

总的来说就是一边自旋获得锁,一边遍历链表,若没有发现重复的节点,则提前新建一个节点,为后面再插入节省时间。如果自旋一定次数没有获得锁,调用lock阻塞。

扩容

超过一定阈值后,Segment内部会进行扩容。在原基础上扩容两倍。

private void rehash(HashEntry<K,V> node) {
    // 记录原来的HashEntry数组信息,表长
    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++) {
        // 记录每一个HahsEntry[]中链表的第一个结点
        HashEntry<K,V> e = oldTable[i];
        // 如果第一个节点不为空,则开始遍历它
        if (e != null) {
            // 记录第一个节点的下一个节点
            HashEntry<K,V> next = e.next;
            // 计算在新数组中的下标位置
            int idx = e.hash & sizeMask;
            // 如果第一个节点的下个节点不存在,则直接将第一个节点转移到新数组中去
            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;
                // 寻找旧表中在新表中顺序不相同的位置,相同则不进行操作
                // 目的是为了减少移动操作
                for (HashEntry<K,V> last = next; last != null; last = last.next) {
                    // 计算原来的hash值再新表中的下标
                    int k = last.hash & sizeMask;
                    // 如果不等于原来数组下标中的位置,则记录它
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                // 将原来不相同的位置上的元素放到新数组中来
                newTable[lastIdx] = 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);
                }
            }
        }
    }
    // 插入新节点
    // 计算将要存放的位置,头插法插入,最后赋值给table
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

扩容后数组变成两倍原数组大小,新的hash值有两种可能,1.原hash值,2.原hash值加上原数组长度。

所以扩容也做了一次优化,对链表尾部一串新hash值相同的一个子链表,不需要一个个移动,只需要将lastRun位置的节点连接到新数组中。

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);
    // 计算当前key在Segemnts数组中的位置
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 如果当前Segments中第u个位置不为空则进行查找
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        // 循环遍历当前key在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;
        }
    }
    // 没有找到则返回null
    return null;
}

整个get过程进行了两次hash,一次对Segment数组hash,一次对HashEntry数组进行hash。

整个读的过程没有加锁,而是使用了UNSAFE.getObjectVolatile函数读取最新值。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值