Java集合:ConcurrentHashMap

上一篇整理过hashmap. 插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

而对应的安全的Hashtable是整个的加锁,所以效率低。还有并发安全的ConcurrentHashMap,jdk1.6、1.7实现的共同点主要是通过采用分段锁Segment减少热点域来提高并发效率。1.8 改变较大,更像是hashmap的结构,但是线程安全与高效。相关的知识点有hash,位运算,unsafe的cas,synchronized,红黑树等等。再次膜拜大神Doug Lea。


一 参数

在深入JDK1.8的put和get实现之前要知道一些常量设计和数据结构。

// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
private static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;//扩容转移时的最小数组分组大小
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1; 
// 树根节点的hash值
static final int TREEBIN   = -2; 
// ReservationNode的hash值
static final int RESERVED  = -3; 
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
private transient volatile int sizeCtl;

二 内部类

2.1 node

Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,是通过 Unsafe 类的 方法进行全部替换,它增加了find方法辅助map.get()方法。简单来说就是个链表,只能查不能改。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    //相比于 HashMap ,加入了 volatile 关键字来保持可见性和禁止重排序
    volatile V val;
    volatile Node<K,V> next;

    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    public final V setValue(V value) {//不允许更新value
        throw new UnsupportedOperationException();
    }
public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }
    //用于map中的get()方法,子类重写
    Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }

2.2 TreeNode

树节点类,当链表长度过长的时候,会转换为TreeNode。与hashmap并不直接用于红黑树的结点,而是将 结点包装成 TreeNode 后,用TreeBin 进行二次包装。
TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,

也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。

static final class TreeNode<K,V> extends Node<K,V> {
    //树形结构的属性定义
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red; //标志红黑树的红节点
    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
    Node<K,V> find(int h, Object k) {
        return findTreeNode(h, k, null);
    }
    //根据key查找 从根节点开始找出相应的TreeNode,
    final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
        if (k != null) {
            TreeNode<K,V> p = this;
            do  {
                int ph, dir; K pk; TreeNode<K,V> q;
                TreeNode<K,V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
        }
        return null;
    }
}

2.3 treebin

TreeBin可以理解为封装TreeNode的容器,不持有key与val ,拥有指向TreeNode 的 列表 list和它们的跟root。
它提供转换黑红树的一些条件和读写锁的控制(用于在树重新构造之前,写入线程去等待读取线程完成)。

可以结合一开始的图理解,实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象.

官方注释:TreeBins do not hold user
     * keys or values, but instead point to list of TreeNodes and
     * their root. They also maintain a parasitic read-write lock
     * forcing writers (who hold bin lock) to wait for readers (who do
     * not) to complete before tree restructuring operations.

static final class TreeBin<K,V> extends Node<K,V> {
    //指向TreeNode列表和根节点
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    volatile Thread waiter;
    //通过锁的状态 , 判断锁的类型。
    volatile int lockState;
    // 读写锁状态
    static final int WRITER = 1; // 获取写锁的状态
    static final int WAITER = 2; // 等待写锁的状态
    static final int READER = 4; // 增加数据时读锁的状态

构造方法:

TreeBin(TreeNode<K,V> b) {  
        super(TREEBIN, null, null, null);  
        this.first = b;  
        TreeNode<K,V> r = null;  
        for (TreeNode<K,V> x = b, next; x != null; x = next) {  
            next = (TreeNode<K,V>)x.next;  
            x.left = x.right = null;  
            if (r == null) {  
                x.parent = null;  
                x.red = false;  
                r = x;  
            }  
            else {  
                K k = x.key;  
                int h = x.hash;  
                Class<?> kc = null;  
                for (TreeNode<K,V> p = r;;) {  
                    int dir, ph;  
                    K pk = p.key;  
                    if ((ph = p.hash) > h)  
                        dir = -1;  
                    else if (ph < h)  
                        dir = 1;  
                    else if ((kc == null &&  
                                (kc = comparableClassFor(k)) == null) ||  
                                (dir = compareComparables(kc, k, pk)) == 0)  
                        dir = tieBreakOrder(k, pk);  
                        TreeNode<K,V> xp = p;  
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {  
                        x.parent = xp;  
                        if (dir <= 0)  
                            xp.left = x;  
                        else  
                            xp.right = x;  
                        r = balanceInsertion(r, x);  
                        break;  
                    }  
                }  
            }  
        }  
        this.root = r;  
        assert checkInvariants(root);  
    }  
    root 代表 TreeNode 的根结点使用first ,是用于第一次初始化时,因为root的特殊性,所以不便于 this.root = b 因此通过 first代替第一次的初始化过程。然后在 过程中 用r 代表root ,直到结束 红黑树的初始化后,再 root =r 保证root的安全性。

可以看到在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识为。同时也看到我们熟悉的红黑树构造方法。

2.4 ForwardingNode

作用是在 transfer() 过程中,插入到bins的头部的结点,用作链接作用,hash值为-1,存储nextTable的引用作为一个占位符放在table中表示当前节点为null或则已经被移动。

 static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer: for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        else
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

三 Unsafe 类 与 常用的操作

java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供了以下功能:

  1. 通过Unsafe类可以分配内存,可以释放内存;
     类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存.
2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;
    JAVA中对象的字段的定位可能通过staticFieldOffset方法实现,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的。
  getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。

  getLong方法获取对象中offset偏移地址对应的long型field的值

 /***
   * Retrieves the value of the object field at the specified offset in the
   * supplied object with volatile load semantics.
   * 获取obj对象中offset偏移地址对应的object型field的值,支持volatile load语义。
   * 
   * @param obj the object containing the field to read.
   *    包含需要去读取的field的对象
   * @param offset the offset of the object field within <code>obj</code>.
   *       <code>obj</code>中object型field的偏移量
   */
  public native Object getObjectVolatile(Object obj, long offset);
数组元素定位:
        Unsafe类中有很多以BASE_OFFSET结尾的常量,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
3、挂起与恢复
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
4 CAS操作
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
感兴趣的可以去看对应的底层源码:Unsafe.h

下面是 ConcurrentHashMap 中有关的应用

// Unsafe mechanics
    private static final sun.misc.Unsafe U;
    //对应于 类中的 sizectl
    private static final long SIZECTL;
    //在 transfer() 方法的使用时,计算索引
    private static final long TRANSFERINDEX;
    // 用于对 ConcurrentHashMap 的 size 统计。
    // 下文 第8点关于 size 会说明。
    private static final long BASECOUNT;
    // 辅助类 countercell 类中的属性,用于分布式计算
    // 是实现  java8 中 londAddr 的基础
    private static final long CELLSBUSY;
    private static final long CELLVALUE;
    // 用来确定在数组中的位置
    // 数组中的偏移地址
    private static final long ABASE;
    // 数组中的增量地址
    private static final int ASHIFT;

    static {
        try {
            //通过反射调用 类中的值,从而对 这些变量赋值
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentHashMap.class;
            SIZECTL = U.objectFieldOffset
                (k.getDeclaredField("sizeCtl"));
            TRANSFERINDEX = U.objectFieldOffset
                (k.getDeclaredField("transferIndex"));
            BASECOUNT = U.objectFieldOffset
                (k.getDeclaredField("baseCount"));
            CELLSBUSY = U.objectFieldOffset
                (k.getDeclaredField("cellsBusy"));
            Class<?> ck = CounterCell.class;
            CELLVALUE = U.objectFieldOffset
                (ck.getDeclaredField("value"));
            Class<?> ak = Node[].class;
            ABASE = U.arrayBaseOffset(ak);
            int scale = U.arrayIndexScale(ak);
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

常用方法:三个原子操作

   //获得 i 位置上的 Node 节点
 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);  
   }  
//利用CAS算法设置i位置上的Node节点。 参见上面unsafe的注释
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } //利用volatile方法设置节点位置的值 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); } 这里没有使用hashmap那种的直接table[index],而使用了Unsafe.getObjectVolatile来获取。
在java内存模型中,我们已经知道每个线程都有一个工作内存,里面存储着table的副本,虽然table是volatile修饰的,
但不能保证线程每次都拿到table中的最新元素,Unsafe.getObjectVolatile可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。其实jdk1.7 也有类似的获取方式,如UNSAFE.getObject(segments, (j << SSHIFT)

四 初始化

实例化ConcurrentHashMap时带参数时,会根据参数调整table的大小,假设参数为100,最终会调整成256,确保table的大小总是2的幂次方。

 public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
    private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

ConcurrentHashMap在构造函数中只会初始化sizeCtl值,并不会直接初始化table,而是延缓到第一次put操作。

我们截取部分put代码

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();

 sizectl 变量hash表初始化或扩容时的一个控制位标识量.参见最开始参数说明。下面是初始化方法

 /** 
     * Initializes table, using the size recorded in sizeCtl. 
     */  
    private final Node<K,V>[] initTable() {  
        Node<K,V>[] tab; int sc;  
        while ((tab = table) == null || tab.length == 0) {  
                //sizeCtl <0 表示有其他线程正在进行初始化操作,把线程挂起。对于table的初始化工作,只能有一个线程在进行。  
            if ((sc = sizeCtl) < 0)  
                Thread.yield(); // lost initialization race; just spin  

                //利用CAS方法把sizectl的值置为-1 表示本线程正在进行初始化  
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {  
                    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];  
                        table = tab = nt;  
                        //相当于0.75*n 设置一个扩容的阈值  
                        // sc = n - n/4
                        sc = n - (n >>> 2);
                    }  
                } finally {  
                    // 更新 sizectl
                    sizeCtl = sc;  
                }  
                break;  
            }  
        }  
        return tab;  
    }  
sizeCtl默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1,有且只有一个线程能够修改成功,其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。
多线程的扩容相对复杂,单独整理一篇。

五 put

put方法依然沿用HashMap的put方法的思想,根据hash值计算这个新插入的点在table中的位置i。

 如果没有初始化就先调用initTable()方法来进行初始化过程
如果没有hash冲突就直接CAS插入
如果还在进行扩容操作就先进行扩容
如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

public V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //不允许 key或value为null  
    if (key == null || value == null) throw new NullPointerException();
    //计算hash值  参见下面1 hash
    int hash = spread(key.hashCode());
    //计算该链表 节点的数量 ,0: 未加入新结点, 2: TreeBin或链表结点数, 其它:链表结点数。主要用于每次加入结点后查看是否要由链表转为红黑树
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {//死循环,CAS经典写法,不成功就重试
        Node<K,V> f; int n, i, fh;
        // 第一次 put 操作的时候初始化,如果table为空的话,初始化table  
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();//参见上面的初始化介绍

        //根据hash值计算出在table里面的位置,参见unsafe介绍   
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 根据对应的key hash 到具体的索引,如果该索引对应的 Node 为 null,则采用 CAS 操作更新整个 table
            // 如果这个位置没有值 ,直接放进去,不需要加锁  
            if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //当前结点正在扩容,扩容完毕再在新table中放入键值对,扩容节细讲
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 结点上锁,只是对链表头结点作锁操作
            synchronized (f) {
                if (tabAt(tab, i) == f) {//双重检查i处结点未变化
                    //fh > 0 说明这个节点是一个链表的节点 不是树的节点  hash值是大于0的,即spread()方法计算而来
                    if (fh >= 0) {
                        binCount = 1;
                        //在这里遍历链表所有的结点  
                        //并且计算链表里结点的数量
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果hash值和key值相同  则修改对应结点的value值  
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent) //是新元素才加入标志位,一般使用不会用到
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            //如果遍历到了最后一个结点,那么就证明新的节点需要插入 就把它插入在链表尾部  next由volatile修饰
                            if ((e = e.next) == null) {
                                // 插入到链表尾
                                pred.next = new Node<K,V>(hash, key,
                                                            value, null);
                                break;
                            }
                        }
                    }
                    //如果这个节点是树节点,就按照树的方式插入值  
                    else if (f instanceof TreeBin) {
                        // 如果是红黑树结点,按照红黑树的插入
                        Node<K,V> p;
                        // 如果为树节点, binCount一直为2,不会引发扩容。
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                        value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                // 如果这个链表结点达到了临界值8,那么把这个链表转换成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);  //下面介绍
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //将当前ConcurrentHashMap的元素数量+1,检测table的扩容
    addCount(1L, binCount);
    return null;
}

1 hash

  static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash  //01111111_11111111_11111111_11111111
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

这里是计算hash,本来hash目的就是为了散列均与,所以除了无符号的右移16位让高位参与异或外,还与hash_bits按位与。

就是为了保证hash是正数(  hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点)。

2  tabAt 

table中定位索引位置,n是table的大小,参见unsafe的方法介绍

helpTransfer

帮助扩容,单独扩容再说

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {  
       Node<K,V>[] nextTab; int sc;  
       // 当前 table 不为 null , 且 f 为 forwardingNode 结点 , 且存在下一张表
       if (tab != null && (f instanceof ForwardingNode) &&  
           (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {  
           int rs = resizeStamp(tab.length);//计算一个扩容校验码  
            // 当 sizeCtl < 0 时,表示有线程在 transfer().
           while (nextTab == nextTable && table == tab &&  
                  (sc = sizeCtl) < 0) {  
                //正常情况下 sc >>> RESIZE_STAMP_SHIFT  == resizeStamp(tab.length);
               if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||  
                   sc == rs + MAX_RESIZERS || transferIndex <= 0)  
                   break;  
                //将 扩容的线程先行减一,表示,这是来辅助 transfer,而非进行 transfer的线程。
               if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {  
                   transfer(tab, nextTab);  
                   break;  
               }  
           }  
           return nextTab;  
       }  
       return table;  
   }  

对应的transefer就不展开。

4 treebybin

 涉及变量 MIN_TREEIFY_CAPACITY = 64;
如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //如果整个table的数量小于64,就扩容至原来的一倍,不转红黑树了
        //因为这个阈值扩容可以减少hash冲突,不必要去转红黑树
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) 
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        //封装成TreeNode
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //通过TreeBin对象对TreeNode转换成红黑树
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

六 get

通过 key值 搜索 value 值。并且要 通过分辨 结点的种类,进行不同形式的寻找。

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算hash值
        int h = spread(key.hashCode());
        //根据hash值确定节点位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点	
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //如果eh<0 说明这个节点在树上 直接寻找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
             //否则遍历链表 找到对应的值并返回
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

七 size()

ConcurrentHashMap的size()方法返回的是一个不精确的值,因为在进行统计的时候有其他线程正在进行插入和删除操作。

 1 辅助定义

为了更好地统计size,ConcurrentHashMap提供了baseCount、counterCells两个辅助变量和一个CounterCell辅助内部类。

  /**
     * Base counter value, used mainly when there is no contention,
     * but also as a fallback during table initialization
     * races. Updated via CAS.
     */
    private transient volatile long baseCount;
     /**
     * A padded cell for distributing counts.  Adapted from LongAdder
     * and Striped64.  See their internal docs for explanation.
     */
    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

2 size()

方法定义如下:

    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

内部调用了上面的sumCount方法,sumCount()就是迭代counterCells来统计sum的过程。

3 mappingCount

官方是推荐用mappingCount替代size的。具体方法都是依赖sumCount。

    public long mappingCount() {
        long n = sumCount();
        return (n < 0L) ? 0L : n; // ignore transient negative values
    }
4 addCount

再来看看put时候调用的addCount,这个也是影响size的

private final void addCount(long x, int check) {  
        //用到了 CounterCell 类
        CounterCell[] as; long b, s;  
        //利用CAS方法更新baseCount的值   
        if ((as = counterCells) != null ||  
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {  
            CounterCell a; long v; int m;  
            boolean uncontended = true;  
            if (as == null || (m = as.length - 1) < 0 ||  
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||  
                !(uncontended =  
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {  
                fullAddCount(x, uncontended);  //并发失败的情况
                return;  
            }  
            if (check <= 1)  
                return;  
            s = sumCount();  
        }  
        //如果check值大于等于0 则需要检验是否需要进行扩容操作  
        //下面的逻辑与 helpTransfer() 类似,可以与 helpTransfer() 一起参考。
        if (check >= 0) {  
            Node<K,V>[] tab, nt; int n, sc;  
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&  
                   (n = tab.length) < MAXIMUM_CAPACITY) {  
                int rs = resizeStamp(n);  
                //如果已经有其他线程在执行扩容操作  
                if (sc < 0) {  
                    //校验失效,直接退出。
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||  
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||  
                        transferIndex <= 0)  
                        break;  
                    
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))  
                        transfer(tab, nt);  
                }  
                //当前线程是唯一的或是第一个发起扩容的线程  此时nextTable=null  
                else if (U.compareAndSwapInt(this, SIZECTL, sc,  
                                             (rs << RESIZE_STAMP_SHIFT) + 2))  
                    transfer(tab, null);  
                s = sumCount();  
            }  
        }  
    }  
  1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

   4 为啥jdk1.8用synchronized替代了ReentrantLock?

  1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然。(可以认为随着jdk的版本优化而优化)
  3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据(因为只有头结点考虑锁,不是每个节点都要。Segment是继承ReentrantLock的。

参考:

https://segmentfault.com/a/1190000010959342

https://zhuanlan.zhihu.com/p/27149377

http://pettyandydog.com/2017/07/27/concurrentHashMap/

https://www.jianshu.com/p/c0642afe03e0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值