ConcurrentHashMap 源码分析,基于JDK1.8

1:几个重要的常量定义

private static final int MAXIMUM_CAPACITY = 1 << 30; //map 容器的最大容量

private static final int DEFAULT_CAPACITY = 16; // map容器的默认大小

private static final float LOAD_FACTOR = 0.75f;  //加载因子

static final int TREEIFY_THRESHOLD = 8;  //由链表转为树状结构的链表长度

static final int UNTREEIFY_THRESHOLD = 6; //由树状结构转为链表

static final int MIN_TREEIFY_CAPACITY = 64; //数组长度最小为64才会转为红黑树

 // 成员变量定义

transient volatile Node<K,V>[] table;  //Node数组 用于存储元素

private transient volatile Node<K,V>[] nextTable; //当扩容的时候用于临时存储数组链表

private transient volatile long baseCount; //保存着哈希表所有节点的个数总和,相当于hash      Map  size

private transient volatile int sizeCtl;

接下来分析几个关键的点:

1:第一次扩容的场景第一次初始化 map 中的table数组

2:在table 成员变量的 i 索引处添加元素 即table[i] 为空的时候添加元素

3:当table[i] 不为空的时候添加元素,即拉链法

4:扩容机制,是如何实现扩容的,如何保证线程安全,在扩容的时候如果这个时候其他线程执行 put和 get的时候会怎样,如何保证线程安全

下面进入几个关键点的具体分析:

在ConcurrentHashMap 进行初始化的时候只是执行一个空的构造方法,对成员变量中的值没有进行初始化操作 即table=null;

1:第一次扩容:当map第一次进行put操作的时候,成员变量table=null 这个时候会进行扩容操作,代码如下:

if (tab == null || (n = tab.length) == 0)
                tab = initTable();

下面主要看下,当线程 t1 进入initTable(); 的时候,这时线程 t2 也符合tab == null添加下则进入 initTable();方法,这个时候如何保证 t1,t2 扩容时候的线程安全;

保证方式:其实首次对map进行扩容的时候,即初始化table变量的时候,只需要保证第一个线程进入时进行初始化,其他线程无法执行即可。

这时通过CAS保证update只有一个线程成功即可。

下面看看 initTable() 这个方法的实现方案:

if ((sc = sizeCtl) < 0)   // 初始化时为0   当为负数的时候线程 进入yield()方法
    Thread.yield();     // yield()方法会通知线程调度器放弃对处理器的占用,当前线程放弃执行权

当t1 是第一次获(sc = sizeCtl) < 0进入这个判断的时候 sizeCtl=0 是不会进入线程放弃执行权的,这时会进入以下的逻辑

        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {  //通过CAS方法将sizeCtl的值更新为-1,这时一个原子操作,只有当原始值是0的时候才能够更新成功
                try {   //因为CAS只有一个线程可以成功  所以一下逻辑保证只有一个线程可以进入执行  可以保证线程安全
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];  // 这里创建一个 n=16 的Node[]数组 并且赋值给table变量
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;   //当 t1首次扩容完成之后 sizeCtl=0 表明扩容完成
                }
                break;
            }

2:在table 成员变量的 i 索引处添加元素 即table[i] 为空的时候添加元素:这个时候的 table[i]==null,只需要保证第一个线程添加成功,

并且对其他线程可见即可 使用 CAS+volatile 即可保证,无需加锁

具体的代码如下: 假设线程 t1 和 线程 t2 同时操作

      else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {  //1:t1 先进入 t2 后进入 当t1 通过 casTabAt 更改过table[i] 为非null 之后,t2线程则进入不了下面的逻辑   2:有可能t1  t2  获取到的 table[i]都是null 这是都会进入下面的逻辑进行操作
                if (casTabAt(tab, i, null,                            // 这里使用casTabAt()方法来更新 table[i] 的值,只有一个线程可以更改成功,这样就保证了只有一个线程操作成功,保证了线程安全。
                    new Node<K,V>(hash, key, value, null)))
                    break;                   // 当 table[i] 的元素为空的时候,不需要通过加锁的方式来进行put操作,减少了开销,而map中进行put操作时 大部分的场景下 table[i]==null 这样避免了频繁加锁和释放锁的开销
            }

tabAt  的具体方法如下: 获取table[i] 的元素  保证可见性

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

3:当table[i] 不为空的时候添加元素,即拉链法 这个时候因为成员变量 table是共享的,所以对table[i] 进行操作的时候需要加锁的,这里使用的是 synchronized 来加锁

加锁的代码如下:

synchronized (f) // f=table[i] 其实就是table数组的第i段,所以这里的加锁粒度也是对 table 数组的某一个需要操作的分段尽心加锁
          if (e.hash == hash &&            //这段逻辑就是对key重复的元素进行覆盖
               ((ek = e.key) == key ||
               (ek != null && key.equals(ek)))) {
                   oldVal = e.val;
                   if (!onlyIfAbsent)
                    e.val = value;
                    break;
                                }

真正执行操作的是下面的逻辑

Node<K,V> pred = e;
if ((e = e.next) == null) {   //当遍历table[i] 中的元素,e为最后一个Node的时候,将新的Node添加到 e.next 元素中  这就完成了拉链法的操作  就是在table[i] 的对象锁下进行操作的。
            pred.next = new Node<K,V>(hash, key,value, null);
            break;
                                }        

4:扩容机制,是如何实现扩容的,如何保证线程安全,在扩容的时候如果这个时候其他线程执行 put和 get的时候会怎样,如何保证线程安全?

我们知道扩容的时候需要 新建一个 nextTable 的Node[]数组,然后就是一个将就的table元素复制到新的nextTable数组中的过程,

这里新建一个nextTable Node[] 时需要保证只有一个线程在操作,这样可以保证线程安全

代码如下: 这里通过compareAndSwapInt 的CAS方法来保证只有一个线程执行下面的transfer 方法

if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);

接下来就是进入了transfer(tab, nt); 的方法中步骤如下:

1: Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //新建一个Node[]数组 长度为原来长度的2倍
  nextTab = nt; //将新的Node 数组指向nextTab
2: 初始化ForwardingNode节点,其中保存了新数组nextTable的引用,在处理完每个槽位的节点之后当做占位节点,表示该槽位已经处理过了;

int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;

3、通过for自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号

for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
            // ... 逻辑代码
            }

4、在当前假设条件下,槽位15中没有节点,则通过CAS插入在第二步中初始化的ForwardingNode节点,用于告诉其它线程该槽位已经处理过了;

else if ((f = tabAt(tab, i)) == null)
      advance = casTabAt(tab, i, null, fwd);

 5、如果槽位15已经被线程A处理了,那么线程B处理到这个节点时,取到该节点的hash值应该为MOVED,值为-1,则直接跳过,继续处理下一个槽位14的节点;

else if ((fh = f.hash) == MOVED)
                advance = true;

6:如果f 是一个链表结构,首先需要对该该链表进行加锁后,遍历链表中的Node 元素,将链表中的元素复制到新的 table 数组中,这里面用到了快速将元素从旧的链表中复制到新的链表中,然后将操作完成的链表索引指向一个ForwardingNode节点,表示操作完成。

7:遍历完成旧的table[]数组中的所有节点之后,完成操作了,将 table引用指向新的table[]数组 完成了扩容的机制扩容的过程也是通过synchronized 加 CAS的方式来保证线程的安全

posted @ 2019-07-19 17:16  beppezhang  阅读(329)  评论(0编辑  收藏  举报