zoukankan      html  css  js  c++  java
  • 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的方式来保证线程的安全

  • 相关阅读:
    LeetCode Fraction to Recurring Decimal
    LeetCode Excel Sheet Column Title
    LeetCode Majority Element
    LeetCode Reverse Nodes in k-Group
    LeetCode Recover Binary Search Tree
    一天一个设计模式(10)——装饰器模式
    一天一个设计模式(9)——组合模式
    一天一个设计模式(8)——过滤器模式
    一天一个设计模式(7)——桥接模式
    一天一个设计模式(6)——适配器模式
  • 原文地址:https://www.cnblogs.com/beppezhang/p/11214495.html
Copyright © 2011-2022 走看看