ConcurrentHashMap(jdk1.7)底层原理

ConcurrentHashMap和HashMap的思路是差不多的,但是因为它支持并发操作,所以要复杂一些。

数据结构

在这里插入图片描述

整个ConcurrentHashMap是由一个一个的Segment组成,Segment代表一个分段,一个Segment里面包含一个HashEntry数组,每个HashEntry是一个链表结构,当对HashEntry数组的数据进行修改的时候,必须首先获得与它对应的Segment锁。

成员变量
 //默认初始容量
 static final int DEFAULT_INITIAL_CAPACITY = 16;
 //默认加载因子(针对Segment数组中的某个Segment中的HashEntry数组扩容)
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 //默认Segment数组的大小,也成为并发量
 static final int DEFAULT_CONCURRENCY_LEVEL = 16;
 //最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;
 //一个Segment的HashEntry数组的最小容量
 static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
 //一个Segment的HashEntry数组的最大容量
 static final int MAX_SEGMENTS = 1 << 16;
 // 锁之前重试次数
 static final int RETRIES_BEFORE_LOCK = 2;
构造方法
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;//segment的偏移
        int ssize = 1;//segment的size  = ssize * 2^sshift
        //计算并行级别,保持并行级别是2的n次方
        while (ssize < concurrencyLevel) {
        	//探讨默认情况下concurrencyLevel=16,sshift=4,ssize经过4此左移,和并行度相等=16
            ++sshift;
            ssize <<= 1;
        }
        //下边这两个变量是为了put方法中的计算key对应Segment数组的索引
        this.segmentShift = 32 - sshift;//-->默认为28
        this.segmentMask = ssize - 1;//-->默认为15
        //initialCapacity 是设置整个map初始的大小
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
       //这里根据initialCapacity 计算segment数组中的每个segment中的HashEntry数组可以分到的大小
       //如initialCapacity =64,那么每个segment中的HashEntry数组就可以分到4个
        int c = initialCapacity / ssize;
        //当不能整除的时候,则让c+1
        if (c * ssize < initialCapacity)
            ++c;
        //默认MIN_SEGMENT_TABLE_CAPACITY=2.这个值也是有用的,因为这样的话,对于具体的HashEntry上,插入一个元素不至于扩容,插入第二个的时候才会扩容
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // 创建Segment数组,并创建数组的第一个元素,segment[0]
        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];
        //将s0写入segment[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

put过程分析

public V put(K key, V value) {
        Segment<K,V> s;
        //判断value是否为null,如果为null则抛出空指针异常
        if (value == null)
            throw new NullPointerException();
        //计算key的hash值
        int hash = hash(key);
        //根据key的hash值计算出在Segment数组中的位置j
        //hash值是32位的,默认情况下先无符号右移28位,剩下高四位,然后&15,还是hash的高四位
        //也就是说j是hash的高4位的值,也就是对应的segment数组中的下标
        int j = (hash >>> segmentShift) & segmentMask;
        //判断该位置是否为null,
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
       		//如果为null,初始化该位置segment[j],通过ensureSegment(j)
            s = ensureSegment(j);
        //调用Segment的put方法将数据插入到HashEntry中
        //见下方
        return s.put(key, hash, value, false);
    }

Segment–put()

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
 			//在往segment写之前,需要先获取该segment的独占锁
 			//获取到了直接返回null
			//获取不到就会进入scanAndLockForPut()方法获取锁,初始化node,,具体我也没看懂
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
            	//table是segment内部的数组,HashEntry类型的
                HashEntry<K,V>[] tab = table;
                //再利用hash,求应该放置的数组下标,和hashmap的一样
                int index = (tab.length - 1) & hash;
                //获取该位置的链表的表头,赋给first
                HashEntry<K,V> first = entryAt(tab, index);
                //一个死循环
                //判断当前位置的链表是否为null,并针对两种情况具体操作
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                    	//如果当前链表中有元素
                        K k;
                        //判断当前key是否和当前链表上的节点的元素相等
                        //如果相等则直接覆盖并跳出循环
                        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 {
                    	//如果当前链表中没有元素
                    	//判断node是否为null
                        if (node != null)
                        	//如果不为null则将元素添加在链表的头节点,并指向当前链表的头结点
                            node.setNext(first);
                        else
                        	//如果node为null则初始化node,并将value传入,next指向当前链表的头节点
                            node = new HashEntry<K,V>(hash, key, value, first);
                        //计数+1
                        int c = count + 1;
                        //判断是否需要扩容
                        //如果当前segment中的元素个数大于扩容阈值并且HashEntry数组的长度小于规定的map最大容量,则进行扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        	//具体后边说
                            rehash(node);
                        else
                        	//如果没有达到扩容的条件,将node放到数组HashEntry数组的index位置
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
            	//释放锁
                unlock();
            }
            //返回旧值
            return oldValue;
        }
put中的关键操作

在初始化ConcurrentHashMap的时候,会初始化第一个分段segment[0],对于其他分段,当put的时候才会进行初始化,通过ensureSegment()方法

 private Segment<K,V> ensureSegment(int k) {
 		//获取到当前的Segment数组
        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]
        	//用来当做一个模板来初始化其他的segmengt
            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);
            //初始化segment[k]内部的HashEntry数组
            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 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
                //对于并发操作使用CAS控制
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

扩容方法:rehash()

扩容条件:put的时候,如果判断该值的插入会导致segment中的元素个数超过阈值,那么先会进行扩容,再插值。

该方法不需要考虑并发,因为到这里的时候,是持有该segment的独占锁

 private void rehash(HashEntry<K,V> node) {
 			HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            //新的容量为旧的容量的2倍
            int newCapacity = oldCapacity << 1;
            //计算新的扩容阈值
            threshold = (int)(newCapacity * loadFactor);
            //创建新数组
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            //新的掩码,为容量-1
            int sizeMask = newCapacity - 1;
            //遍历数组,将原数组位置i处的链表拆分到新数组位置i和i+oldCapacity两个位置
            for (int i = 0; i < oldCapacity ; i++) {
            	//e为链表的第一个元素
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                	//如果e不为Null
                    HashEntry<K,V> next = e.next;
                    //计算应该放置在新数组中的位置
                    //假设原数组长度为16,e在oldTable[3]处,那么idx只可能是3或者3+16=19
                    //因为大多数HashEntry中的节点在扩容前后可以保持不变,rehash方法中会定位第一个后续所有节点在扩容后index都保持不变的节点,然后将这个节点之前的所有节点重排即可
                    int idx = e.hash & sizeMask;
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // 重复利用一些扩容后,位置不变的节点,这些节点在原先链表的尾部
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //这个for循环就是找到第一个后续节点新的index不变的节点。
                        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;
                        // 第一个后续节点新index不变节点前的所有节点都需要重新创建分配
                        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放到新数组中刚刚的两个链表之一的头部
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

仔细一看发现,如果没有第一个 for 循环,也是可以工作的,但是,这个 for 循环下来,如果 lastRun 的后面还有比较多的节点,那么这次就是值得的。因为我们只需要克隆 lastRun 前面的节点,后面的一串节点跟着 lastRun 走就是了,不需要做任何操作。

我觉得 Doug Lea 的这个想法也是挺有意思的,不过比较坏的情况就是每次 lastRun 都是链表的最后一个元素或者很靠后的元素,那么这次遍历就有点浪费了。不过 Doug Lea 也说了,根据统计,如果使用默认的阈值,大约只有 1/6 的节点需要克隆。

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;
        //根据key的hash找到对应的segment的位置,并判断segment中的HashEntry数组是否为null
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            //对segment中的数组的每个链表进行遍历
            //这里第一次初始化通过getObjectVolatile获取HashEntry时,获取到的是主存中最新的数据
            //但是在后续遍历过程中有可能被其他线程修改,从而导致这里返回的可能是过时的数据
            //所以这里就是ConcurrentHahsMap的弱一致性的体现,containsKey方法也一样
            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;
    }

并发问题分析

看完了put和get过程,可以看到get过程并没有加锁。

添加节点的操作put和删除节点的操作remove都是加上segment上的独占锁的,所以它们之前自然不会有问题,我们需要考虑的问题就是,get的时候在同一个segment中发生了put或remove操作。

1、 put操作的线程安全性

  • 添加节点到链表的操作时插入到表头的,所以如果这个时候get操作在链表的过程中已经到了中间是不会被影响的。另一个并发问题就是get操作在put之后,需要保证刚刚插入表头的节点被读取,这个依赖于setEntryAt方法中使用的UNSAFE.putOrderedObject.
  • 扩容:扩容是新创建了数组,然后进行迁移数据,最后将newTable设置给属性table, 所以如果get操作此时也在进行,那么也没关系,如果get先行,那么就是在旧的table上做查询操作;而put先行,那么put操作的可见性保证就是table使用了volatile关键字.

2、 remove 操作的线程安全性

  • 如果remove破坏的节点get操作已经过去了,那么这里不存在任何问题
  • 如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹氿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值