基于1.7的ConcurrentHashMap

一个ConcurrentHashMap由一个Segment数组构成,一个Segment由一个HashEntry数组构成,并且不允许null键和null值。因此整体的ConcurrentHashMap结构如下:
在这里插入图片描述
可以说,ConcurrentHashMap是一个二级哈希表,在一个总的哈希表下面,有若干个子哈希表。ConcurrentHashMap的优势就是采用了锁分段技术,每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不影响。

  1. 不同Segment的并发写入:
    在这里插入图片描述
  2. 同一Segment的一写一读:
    在这里插入图片描述
  3. 同一Segment的并发写入:Segment的写入是需要上锁的,因此对同一个Segment的并发写入会被阻塞。
    在这里插入图片描述
    由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁,在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

ConcurrentHashMap的具体操作如下:
Get方法:注意整个get操作只做了一次hash操作,由于HashEntry中的value属性是用volatile关键字修饰的,保证了内存可见性,所以每次获取时都是最新的值,整个过程不需要加锁。

  1. 对输入的key做Hash运算,得到hash值;
  2. 通过hash值,定位到对应的Segment对象;
  3. 再次通过hash值,定位到Segment当中数组的具体位置。

源码如下:

public V get(Object key) {
        Segment<K,V> s; 
        HashEntry<K,V>[] tab;
        //1,得到hash值
        int h = hash(key);
        //2,通过hash值,定位到对应的Segment对象
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //3,获取对应的Segment,
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) {
        	//4,获取Segment对应的HashEntry<K,V>[]数组,并遍历该数组
            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方法:

  1. 对输入的key做hash运算,得到hash值;
  2. 通过hash值,定位到对应的Segment对象;
  3. 对该Segment对象进行加锁操作(ReentrantLock可重入锁);
  4. 再次通过hash值,定位到Segment当中数组的具体位置;
  5. 插入或覆盖HashEntry对象;
  6. 释放锁。
    源码如下:
public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)   throw new NullPointerException();
        //1,一次hash运算
        int hash = hash(key);
        //2,根据hash值得到对应的Segment的位移
        int j = (hash >>> segmentShift) & segmentMask;
        //3,确定Segment
        if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) 
            s = ensureSegment(j);
            //4,对Segment做put操作
        return s.put(key, hash, value, false);
    }

//对Segment做put操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
			//5,获取可重入锁,只有在调用时它是空闲的才能获取锁;如果锁被其他线程占用,该线程会尝试自旋获取锁,最大次数是64,如果达到最大次数,则改为阻塞获取锁,lock()操作,保证能获取成功。
            HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                //6,根据hash值定位到Segment当中数组的具体位置
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                //7,遍历该链表
                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 {
                    	//8,该数组位置的元素为null,创建HashEntry对象
                        if (node != null) { node.setNext(first); }
                        else  { node = new HashEntry<K,V>(hash, key, value, first); }
                        int c = count + 1;
                        //9,元素个数达到阀值,考虑扩容,注意扩容扩的是某个Segment对应的HashEntry数组
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY) { 
                        	rehash(node);
                         }
                        //10,不扩容,则put操作
                        else { setEntryAt(tab, index, node); }
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
            	//11,释放锁
                unlock();
            }
            //12,返回旧值
            return oldValue;
        }

下面是rehash的代码:参数为新的结点node

private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;
            threshold = (int)(newCapacity * loadFactor);
            //扩容扩的是某个Segment对应的HashEntry数组
            HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            //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;
                    //2,链表就一个结点,直接放到新数组
                    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) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        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);
                        }
                    }
                }
            }
            //3,添加新节点
            int nodeIndex = node.hash & sizeMask; 
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

最后一个问题:既然每一个Segment都各自加锁,那么在调用size()方法的时候,怎么解决一致性的问题?
size()方法的目的是统计ConcurrentHashMap的总元素,自然需要把各个Segment内部的元素数量汇总起来。但是如果在统计过程中,已经统计过的Segment瞬间插入新的元素,这时候该怎么办?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ConcurrentHashMap的size方式是一个循环嵌套,大致逻辑如下:

  1. 遍历所有的Segment;
  2. 将Segment的元素数量累加起来;
  3. 把Segment的修改次数累加起来;
  4. 判断Segment总的修改次数是否大于上次总的修改次数,如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果相等,说明没有修改,统计结束;
  5. 如果尝试次数超过阀值,则对每一个Segment加锁,再重新统计;
  6. 再次判断Segment总的修改次数是否等于上次的总的修改次数。由于加锁了,所以一定相等;
  7. 释放锁,统计结束。
    源码如下:
public int size() {
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
            	//1,重试次数达到RETRIES_BEFORE_LOCK = 2次,对所有Segment加锁,由于retries是从-1开始的,所以加锁之前总共进行了3次尝试
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                    	//2,统计总的修改次数和元素个数
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                //2,last的初始值是0,所以第一次if判断为false,last被赋值为sum,继续循环,如果接下来的循环总的修改次数和上次修改次数相等,则说明该统计过程中没有修改操作,结束统计
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
        	//3,retries在加锁的过程中做了+1操作,所以retries > RETRIES_BEFORE_LOCK;对每个Segment释放锁
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        //4,返回size
        return overflow ? Integer.MAX_VALUE : size;
    }

为了尽量不锁住所有的Segment,首先会乐观的假设size过程是不会有修改。当尝试2次后,才无奈的转为悲观锁,锁住所有的Segment保证数据的一致性。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值