浅谈ConcurrentHashMap(基于JDK1.7)

浅谈ConcurrentHashMap(基于JDK1.7)

  1. ConcurrentHashMap简介

ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现
ConcurrentHashMap在并发编程的情况下使用率非常高
相较于HashTable每次添加的重量级锁synchronized,锁的粒度更细
1.7的ConcurrentHashMap的value是不支持为null的,这与添加是对值进行非空校验有关

  1. ConcurrentHashMap的数据结构

这里是引用
整体结构就是通过一个个的分段锁来进行并发时的数据隔离
同时Segment锁继承自ReentrantLock,同样继承了他可重入的特性,默认的concurrentlevel为16,代表最大支持十六个线程并发执行

  1. 首先介绍Segment和HashEntry
//对于没一个segment数组来说,里面都维护了一个HashEntry数组
//在并发的情况下,操作不同的segment是不需要考虑竞争锁的问题的
final Segment<K,V>[] segments;

//每个segment数组维护了这么一个HashEntry数组
transient volatile HashEntry<K,V>[] table;

//hashentry的属性
static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
        //其他省略
}

//segment的构造方法
public Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;//负载因子
            this.threshold = threshold;//阈值
            this.table = tab;//主干数组即HashEntry数组
}

//他来了他来了!ConcurrentHashMap的构造方法
public ConcurrentHashMap(int initialCapacity,
                               float loadFactor, int concurrencyLevel) {
          if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
              throw new IllegalArgumentException();
          //MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
          //这里可以这样理解,就是segment的最大取值
          //我们传入的再大,最高也就是最高限值取值,也就是65535
          if (concurrencyLevel > MAX_SEGMENTS)
              concurrencyLevel = MAX_SEGMENTS;
          //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
          //这里就是对其次幂的一种初始化操作
         int sshift = 0;
         //ssize 为segments数组长度,根据concurrentLevel计算得出
         //这里同样可以认为是对数组长度的初始化
         int ssize = 1;
         //这里就是循环对来对次幂进行递增,并且对数组长度进行2次幂的计算
         //最终得到的,数组长度一定是一个2的次幂数值
         while (ssize < concurrencyLevel) {
             ++sshift;
             ssize <<= 1;
         }
         //segmentShift和segmentMask这两个变量在
	    //定位segment时会用到,后面会详细讲
         this.segmentShift = 32 - sshift;
         this.segmentMask = ssize - 1;
         if (initialCapacity > MAXIMUM_CAPACITY)
             initialCapacity = MAXIMUM_CAPACITY;
         // 计算cap的大小,即Segment中HashEntry的数组长度,
	     // cap也一定为2的n次方
	     //这里c代表着每个segment应该放置的HashEntry数组长度
         int c = initialCapacity / ssize;
         //到这一步可能大家会有一个误解,刚才是整除了,为什么有乘回来判断大小呢
         //这里是因为可能有余数,并没有加上,但又不能不要,所以就累加了回来
         if (c * ssize < initialCapacity)
             ++c;
         //这里cap代表着segment存放数组的最小取值
         int cap = MIN_SEGMENT_TABLE_CAPACITY;
         //判断只要他的数值小于我们之前计算的存放数量,就乘2,最后得到的一定是一个2的次幂的数
         while (cap < c)
             cap <<= 1;
         // 创建segments数组并初始化第一个Segment,
	    // 其余的Segment延迟初始化
         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];
         UNSAFE.putOrderedObject(ss, SBASE, s0);
         this.segments = ss;
}

put方法

public V put(K key, V value) {
        Segment<K,V> s;
        //这里就是对value的校验,这里表明了value的数值不能为null
        if (value == null)
            throw new NullPointerException();
        //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
        int hash = hash(key);
        //返回的hash值无符号右移segmentShift位与段掩码进行位运算,
	   // 定位segment
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)
			 // nonvolatile; recheck
			 UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)
			//  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
}

关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
PS:此处是借鉴别人的,没想出来该怎么描述

put方法具体实现

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
			//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),其实也就是起到了一个预热的作用,以求获取锁之后能够快速的定位到位置,若找不到,则创建HashEntry。
			// tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
            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,可以看到,
				//这个hash值在定位Segment时和在Segment
				//t中定位HashEntry都会用到,
				//只不过定位Segment时只用到高几位。
                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;
						//若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

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
    //如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,
    //这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,
    //这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //获取锁失败,初始时retries=-1必然开始先进入第一个if
        if (retries < 0) {//<1>
            if (e == null) { //<1.1>
                //e=null代表两种意思,第一种就是遍历链表到了最后,仍然没有发现指定key的entry;
                //第二种情况是刚开始时确实太过entryForHash找到的HashEntry就是空的,即通过hash找到的table中对应位置链表为空
                //当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,
                //然后进行循环尝试获取锁,在循环次数还未达到<2>以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,
                //那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,
                //所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))//<1.2>   遍历过程发现链表中找到了我们需要的key的坑位
                retries = 0;
            else//<1.3>   当前位置对应的key不是我们需要的,遍历下一个
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {//<2>
            // 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁,
            //之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对系统性能有消耗的,
            //这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {//<3>
            // 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

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;
            //只有单个节点的时候就把当前节点放在新数组的对应位置
            if (next == null)   
                newTable[idx] = e;
            else { 
            	//下面还存在链表的时候
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                //会for循环这个链表
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }//这个for循环就是找到第一个后续节点新的index不变的节点。
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                //第一个后续节点新index不变节点前所有节点均需要重新创建分配。
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

在我们进行各种操作的同时,经常用到了Unsafe类本地方法
关于ConcurrentHashMap的实现,不论是在jdk1.7还是jdk1.8版本中ConcurrentHashMap中使用的最为核心也是最为频繁的就是Unsafe类中的各种native本地方法。主要的几个方法是Unsafe.putObjectVolatile(obj,long,obj2)、 Unsafe.getObjectVolatile、 Unsafe.putOrderedObject等
其实可以知道的就是对这些方法添加了读写屏障,保证了每次读到的一定是内存数据而不是缓冲保存的数据,同时也保证了指令不会被重排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值