ConcurrenHashMap源码(JDK1.7)

世味年来薄似纱,谁令骑马客京华。
小楼一夜听春雨,深巷明朝卖杏花。
矮纸斜行闲作草,晴窗细乳戏分茶。
素衣莫起风尘叹,犹及清明可到家。

——陆游《临安春雨初霁》

 一、前言

1.1 ConcurrentHashMap的锁分段技术

     HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

二、JDK1.7中ConcurrentHashMap

2.1 初识ConcurrentHashMap

我们通过ConcurrentHashMap的类图来分析ConcurrentHashMap的结构:

ConcurrentHashMap类图

JDK1.7中ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

2.2 Segment(锁分段)

ConcurrentHashMap结构图

  • 在ConcurrentHashMap中是使用多个哈希表,具体为通过定义一个Segment来封装这个哈希表其中Segment继承于ReentrantLock,故自带lock的功能。即每个Segment其实就是相当于一个HashMap,只是结合使用了ReentrantLock来进行并发控制,实现线程安全。
  • Segment定义如下:ConcurrentHashMap的一个静态内部类,继承于ReentrantLock,在内部定义了一个HashEntry数组table,HashEntry是链表的节点定义,其中table使用volatile修饰,保证某个线程对table进行新增链表节点(头结点或者在已经存在的链表新增一个节点)对其他线程可见。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    transient volatile HashEntry<K,V>[] table;
    ... 
}
  •  HashEntry的定义如下:包含key,value,key的hash,所在链表的下一个节点next
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
    
    ...
}

 由定义可知,value和next均为使用volatile修饰,当多个线程共享该HashEntry所在的Segment时,其中一个线程对该Segment内部的某个链表节点HashEntry的value或下一个节点next修改能够对其他线程可见。而hash和key均使用final修饰,因为创建一个链表节点HashEntry,是根据key的hash值来确定附加到哈希表数组table的某个链表的,即根据hash计算对应的table数组的下标,故一旦添加后是不可变的。

2.2.3 Segment的哈希表table数组的容量 

MIN_SEGMENT_TABLE_CAPACITY:table数组的容量最小量,默认为2

static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

2.3 成员变量

    //段掩码
    final int segmentMask;

    //段偏移量
    final int segmentShift;

    /**
     * The segments, each of which is a specialized hash table.
     */
    final Segment<K,V>[] segments;

    transient Set<K> keySet;
    transient Set<Map.Entry<K,V>> entrySet;
    transient Collection<V> values;

初始化segmentShift和segmentMask。这两个全局变量在定位segment时的哈希算法里需要使用sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。segmentShift用于定位参与hash运算的位数,segmentShift等于32减sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的,后面的测试中我们可以看到这点。segmentMask是哈希运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1。因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,对应的二进制是16位,每个位都是1。

(都是在默认情况下,concurrencyLevel=16,ssize=16,sshift=4)

那么    

this.segmentShift = 32 - sshift=28 

而this.segmentMask = ssize - 1=15    二进制就是1111;

hash >>> segmentShift) & segmentMask//定位Segment所使用的hash算法

int index = hash & (tab.length - 1);// 定位HashEntry所使用的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。

2.4 ConcurrentHashMap构造方法

在ConcurrentHashMap的构造函数定义实际大小:使用ConcurrentHashMap的整体容量initialCapacity除以Segments数组的大小,得到每个Segment内部的table数组的实际大小。

public ConcurrentHashMap() {
        //DEFAULT_INITIAL_CAPACITY:16;DEFAULT_LOAD_FACTOR:0.75;DEFAULT_CONCURRENCY_LEVEL:16
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

构造方法中入参的含义:

  • initialCapacity:表示整个ConcurrentHashMap的所有Segment加起来含有Entry的总量。注意不是segment的数量。这里与HashMap类似,实际创建的Entry数量应该为大于initialCapacity的最小的2的n次方。
  • concurrentcyLevel:并发级别,它用来确定segment的个数,segment的个数 >= concurrentcyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的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;
    
    // ssize:segments数组的大小
    // 不能小于concurrencyLevel,默认为16
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
        
    // cap:Segment内部HashEntry数组的大小
    // 最小为MIN_SEGMENT_TABLE_CAPACITY,默认为2
    // 实际大小根据c来计算,而c是由上面代码,
    // 根据initialCapacity / ssize得到,
    // 即整体容量大小除以Segment数组的数量,则
    // 得到每个Segment内部的table的大小
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[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];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

这段代码比较长,我们选择代码块逐个分析:

1.代码块一

    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
<<运算规则:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。
  语法格式:
  需要移位的数字 << 移位的次数
  例如: 3 << 2,则是将数字3左移2位
  计算过程:
  3 << 2

初始情况下 ssize=1 concurrencyLevel=16

 ssize <<= 1;

第一次循环 ssize=1<16 进入循环 ssize左移1位变成2;
第二次循环 ssize=2<16 进入循环 ssize左移1位变成4;
第三次循环 ssize=4<16 进入循环 ssize左移1位变成8;
第四次循环 ssize=8<16 进入循环 ssize左移变成16;

此时 ssize变成了16 不小于16 跳出循环。

这块代码的目的是去找到一个最小的大于等于concurrencyLevel的2的幂次方数
大于等于16的最小2的幂次方数就是16呀 所以我们找到ssize就是16
同时 心细一点的同学跟着刚才的循环去计算 得到的sshift是4 也就是找到了2^4等于16的这个4

2.代码块2

我们先注释掉一部分不去管它

if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    /*if (c * ssize < initialCapacity)
        ++c;*/
 int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;

这块代码就要引出ConcurrentHashMap的底层结构了 要认真理解这块。

//initialCapacity的默认值是16
//ssize上面我们算出来是16
//我们可以得到下面这个写的不太规范的式子
int c=16/16=1

这里的c是什么呢 ? 这里的c是用来计算Segment数组的大小,segment数组的大小是2的n次方,且第一个大于c的2的n次方。

int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;

这个MIN_SEGMENT_TABLE_CAPACITY的**默认值是2(也就是说设计者设计这个Segment数组最小长度就是2) **就是说现在cap=2;c=1;
2不小于1 所以不会进入这个while循环 接着往下,
我们这里算的c=1 比设计者设计的Segment数组最小长度还要小 所以我们按cap=2去初始化

在这里插入图片描述

如上图所示 cap指定了每一个Segment可以放几个HashEntry
ssize指定了一个ConcurrentHashMap可以放多少个Segment

现在 我们就可以重新绘制一下这个数据结构图了,
这其实就是默认情况下 ConcurrentHashMap的数据结构了:

在这里插入图片描述


 这里 我们把刚刚注释掉的代码打开 再去分析一波:

int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;

假设 我这里指定了initialCapacity是33,
那我们这个时候33/16 计算出来得c就是2 这个时候就会进入if这里 c就会加1 此时c等于了3

while (cap < c)
            cap <<= 1;

当代码走到这里 cap=2 c=3 2<3,就会进入while当中 cap左移变成了4 这里实质性的改变就是对应的每一个Segment就会有4个HashEntry。
这里说明一下 为什么cap使用了左移 从2变成了4 这是因为设计者要Segment的大小不论是几都应该是2的幂次方数。
此时的数据结构是这样:

在这里插入图片描述

 注意:

在构造方法里创建了Segments[],和第一个Segment对象segment[0]。其他的segment对象为null,这属于懒加载。那么为啥要把Segment[0]创建出来,而不是都不初始化,都进行懒加载呢?这是因为Segment[0]中存储了HashEntry数组的长度,每次添加一个键值对的时候,如果发现Segment为null就要新建Segment对象,那么其中的HashEntry数组的长度就直接看Segment[0]中存储了HashEntry数组的长度就可以了,不用每次都按照公式计算一遍。Segment[0]相当于一个模板。详细可参见下面put代码。

2.5 ConcurrentHashMap的put方法

  1. 根据key计算出hashcode。
  2. 确定segment数组的位置:hashcode & segments.length-1
  3. 确定HashEntry数组的位置: hashcode & HashEntry.length-1
public V put(K key, V value) {
        Segment<K,V> s;
        //value不能为null,为null抛异常
        if (value == null)
            throw new NullPointerException();
        //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
        int hash = hash(key);
        //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
        //(1)  j是算出来的Segment数组的下标
        int j = (hash >>> segmentShift) & segmentMask;
        //(2)  通过Unsafe类去取segments数组第j个位置的元素看是不是null
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            //(3) 如果是null 去生成一个Segment对象
            s = ensureSegment(j);
        //(4) 去调用生成的Segment对象的put方法
        return s.put(key, hash, value, false);
    }

小问题:

1.(j << SSHIFT) + SBASE就是segments数组中该segment的位置!
2.(j << SSHIFT) + SBASE:中SBASE值是怎么来的?

从源码看出,put的主要逻辑也就两步:

  1. 定位segment并确保定位的Segment已初始化
  2. 调用Segment的put方法。

 我们把(3)处的ensureSegment方法展开说明一下 去理解他是怎么生成一个Segment对象并保证线程安全的:

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) {
            //这部分代码就是将一开始构造方法生成的ss[0]作为一个原型(雏形)
            //利用ss[0]去初始化我们此时的Segment对象
            //但是真正初始化在下面一个if之后 这里可以理解是做了一个准备工作
            //准备了一些需要的属性 如负载因子啊 cap长度啊
            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);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                //这时候又再次判断该位置有没有其他线程进行了初始化
                //没有其他线程的话   这时候真正去创建一个Segment对象
                //但是这里还没有把Segment对象放到数组对应的位置
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    //这里的这个CAS操作真正对第u个位置进行赋值
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

上述代码用到了双重检查锁(double check)。

由上面代码可知创建segment对象是怎么保证线程安全的:

1.用到了双重检查锁(double check);

2.赋值的时候 利用了CAS这个原子操作。

CAS这个原子操作是不能被中断的 我们这里简单谈一下CAS干了什么。
CAS就是先获取主物理内存中的值作为期望值 然后我们再去获得此时主物理内存的真实值 如果期望值与真实值一致 我们就进行修改 否则 就一直取值比较 直到成功

在这里插入图片描述

再看看 (4) 处代码,去调用生成的Segment对象的put方法:

//(4) 去调用生成的Segment对象的put方法
        return s.put(key, hash, value, false);

 我们去继续学习这个Segment对象的put方法

这里我们先去抽象出一个数据结构 也就是说Segment内部维护的一个个HashEntry整合起来 就好像一个小的HashMap一样 也是数组+链表的形式(Segment内部就像一个小的HashMap)

在这里插入图片描述

我们先不去看加锁的逻辑 我们先把中间怎么put数据的流程大致理解清楚:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //先尝试对segment加锁,如果直接加锁成功,那么node=null;如果加锁失败,则会调用scanAndLockForPut方法去获取锁,
            //在这个方法中,获取锁后会返回对应HashEntry(要么原来就有要么新建一个)
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                //取tab数组 下表为index的值作为first
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    //链表的头结点不为空的情况
                    if (e != null) {
                        K k;
                        //遍历当前位置的链表
                        //判断传入的key 和当前遍历的 key 是否相等,相等则覆盖旧的 value
                        //这里根HashMap的逻辑很像
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    //为空的情况
                    //情况1 头结点为空 把key-value放在头结点
                    //情况2 遍历完整个链表 然后头插法插入
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            //生成了一个HashEntry对象 记为node
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //如果超过阈值 就rehash
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            //没有超过阈值 就把刚刚生成的node通过setEntryAt这个方法放进去
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

由于Segment的put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。第一个put方法已经定位到Segment,然后这个方法中就是在Segment里进行插入操作。此方法插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。

a.是否需要扩容

  在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

b.如何扩容

  在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容

c.对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置。

	
static class Segment<K,V> extends ReentrantLock implements Serializable {

  从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行初始化,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

我们上面初步分析了一下 put添加数据的过程

下面我们重点分析一下加锁的过程

HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);

 

lock()——是一个无条件的锁,与synchronize意思差不多。
tryLock()——ReentrantLock类特有的获取锁的方法。
unlock()——ReentrantLock类特有的释放锁的方法。

trylock()这个方法 如果能够获取到锁 就会立马返回一个true
trylock()这个方法 如果获取不到锁 就会立马返回一个false
trylock()不会阻塞
lock()这个方法如果获取不到锁 就会一直阻塞在这里

进一步了解请移步: Java高级-线程同步lock与unlock使用
阿里面试实战题2----ReentrantLock里面lock和tryLock的区别

 这个scanAndLockForPut方法大概干了什么事情 我给大家解释一下:

当trylock()获取不到锁的时候 通过刚刚我们的铺垫我们知道trylock()是不会阻塞的
那我们不能傻傻的等在这里 我们既然不会阻塞 我们在这个过程中可以准备一些什么事情呀?
这个过程就好比做饭 你在烧水等水开的过程中 可以去准备个凉菜 算是合理安排 提高效率。
我们这里的合理安排就是根据key-value去new一个HashEntry 我们把这个HashEntry记成node。

这里我们就把scanAndLockForPut这个方法做的事情给大家大致说明白了,scanAndLockForPut这个方法本身就设计的非常精妙 由于篇幅的原因就不在展开描述,之后会有更加详细的说明解释。

 tryLock() ? null : scanAndLockForPut(key, hash, value);
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            //获取k所在的segment中的HashEntry的头节点(segment中放得是HashEntry数组,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
            //尝试获取k所在segment的锁。成功就直接返回、失败进入while循环进行自旋尝试获取锁
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    //所在HashEntry链表不存在,则根据传过来的key-value创建一个HashEntry
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    //找到要放得值,则设置segment重试次数为0
                    else if (key.equals(e.key))
                        retries = 0;
                    else //从头节点往下寻找key对应的HashEntry
                        e = e.next;
                }
                //超过最大重试次数就将当前操作放入到Lock的队列中
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                //如果retries为偶数,就重新获取HashEntry链表的头结点
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

这里不是有个三目运算符吗 trylock()获取不到锁的时候 就会走scanAndLockForPut这个方法准备一个node对象出来 。

我的理解

如果tyrLock()未获取到锁,会调用scanAndLockForPut方法,会新建一个node对象,并调用  while (!tryLock()){}  自旋获取锁,索取到锁后,再继续往下走

我们理解一下这里的保证线程安全 为什么用了trylock

在这里插入图片描述

如上图所示 我们根本的目的是把key-value放进去

我们要在链表当中去插入元素 注意是插入元素 这个时候CAS就没有更好的办法了 因为CAS对某一个具体的位置赋 值还是可以的 但是让CAS去插入是不能实现的 所以这个插入时候我们为了保证线程安全 就要去加锁。
这里保证线程安全的方法很实在 就是加了一把锁 让同一时间只有一个线程去put数据。

我们总结一下jdk7 下 ConcurrentHashMap是怎么保证并发安全的:

1. 在进行一些链表的插入数据时 用了ReentrantLock去加了一把锁
2.用了UNSAFE的各种方法 这其中包括了我们最熟悉的CAS 还有UNSAFE类的一些其他方法呀
比如 UNSAFE.putOrderedObject等等

2.6 get方法

public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            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;
    }

 get 没有加锁,因此效率高
注意:get方法使用了getObjectVolatile方法读取segment和hashentry,保证是最新的,具有锁的语义,可见性。

分析:为什么get不加锁可以保证线程安全

(1) 首先获取value,我们要先定位到segment,使用了UNSAFE的getObjectVolatile具有读的volatile语义,也就表示在多线程情况下,我们依旧能获取最新的segment.
(2) 获取hashentry[],由于table是每个segment内部的成员变量,使用volatile修饰的,所以我们也能获取最新的table.
(3) 然后我们获取具体的hashentry,也时使用了UNSAFE的getObjectVolatile具有读的volatile语义,然后遍历查找返回.

总结:我们发现整个get过程中使用了大量的volatile关键字,其实就是保证了可见性(加锁也可以,但是降低了性能),get只是读取操作,所以我们只需要保证读取的是最新的数据即可.

2.7 size方法

先采用不加锁的方式,连续计算元素的个数,最多计算3次:
1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;

public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        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 (;;) {
                /判断retries是否等于RETRIES_BEFORE_LOCK(值为2)
			    //也就是默认有两次的机会,是不加锁来求size的
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                //遍历Segments[]数组获取里面的每一个segment,然后对modCount进行求和
		    	//这个for嵌套在for(;;)中,默认会执行两次,如果两次值相同,就返回
			    //如果两次值不同,就进入到上面的if中,进行加锁。之后在进行求和
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

2.8 contains (同containsValue)

与size方法类似。具体代码略

2.9 containsKey

与get方法类似。具体代码略

2.10 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)   //  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);
                        }
                    }
                }
            }
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

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

假设原来 table 长度为 4 ,那么元素在 table 中的分布是这样的

扩容后 table 长度变为 8 ,那么元素在 table 中的分布变成:

可以看见 hash 值为 34 和 56 的下标保持不变,而 15,23,77 的下标都是在原来下标的基础上 +4 即可,可以快速定位和减少重排次数。该方法没有考虑并发,因为执行该方法之前已经获取了锁。

2.11 remove

与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。

三、 ConcurrentHashMap 的弱一致性

然后对链表遍历判断是否存在 key 相同的节点以及获得该节点的 value 。但由于遍历过程中其他线程可能对链表结构做了调整,因此 get 和 containsKey 返回的可能是过时的数据,这一点是 ConcurrentHashMap 在弱一致性上的体现。如果要求强一致性,那么必须使用 Collections.synchronizedMap() 方法。

四、总结

4.1 jdk1.7的ConcurrentHashMap保证并发安全的措施

4.1.1 整体

1.分段锁(Segment)和重入锁(RetrantLock)。

2.volatile修饰属性:

  1. Segment类中——volatile HashEntry<K,V>[] table属性
  2. HashEntry类中——volatile V value;
  3. HashEntry类中——volatile HashEntry<K,V> next;

3.cas——UNSAFE.compareAndSwapObject(例:ensureSegment方法)

4.在代码中使用UNSAFE.getObjectVolatile方法。

 UNSAFE.getObjectVolatile:

附加了'volatile'加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。

4.1.2 put方法

在put方法中主要有两处做了保证并发的措施:

1.ensureSegment方法

  • 双重检查锁
  • 属性可见——UNSAFE.getObjectVolatile
  • CAS——UNSAFE.compareAndSwapObject

2.put过程

  • 锁——RetrantLock.tryLock()和RetrantLock.lock()

4.1.3 get方法 

  • UNSAFE.getObjectVolatile

 

4.1.4 size方法

  • 属性可见——UNSAFE.getObjectVolatile
  • 锁——RetrantLock.lock()

 

五、最后

UNSAFE.getObject() 方法,移步: Unsafe API介绍及其使用

JAVA中神奇的双刃剑--Unsafe

我觉得这一篇文章很好:ConcurrentHashMap底层详解(JDK1.7)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值