ConcurrentHashMap源码学习笔记(jdk1.8)

为什么要使用ConcurrentHashMap?

首先我们先讨论一下为什么要使用ConcureentHashMap,为了线程安全?确实是为了线程安全,但不知这个原因,因为HashMap在多线程下不安全,但是HashTable是线程安全的,完全可以使用HashTable,那为什么不呢?这是因为HashTable相比HashMap在所有方法上都添加了synchronized关键字来修饰,即:HashTable是对整个table数组进行了锁定,即在一个线程访问HashTable的同步方法时,另外一个线程访问HashTable的同步方法时便会进入阻塞或轮询状态。这就造成了HashTable在多线程竞争激烈的情况下的效率低下。例如:线程1访问HashTable的put方法,线程2的任何同步方法都不能使用,put、get等方法都不能使用。

而ConcurrentHashMap解决了这两个问题。既然给整个table表加一把锁效率低下,那么把一把锁分为好多把锁,把table数组分段,让每一把锁负责一部分数据,这样如果是访问不同锁的时候就不会产生竞争,提高了效率。这就ConcurrentHashMap的锁分段技术的思想。在jdk1.7中就是采用锁分段技术,ConcurrentHashMap由多个Segment组(Segment下包含很多Node,也就是我们的键值对了),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。具体实现可以搜索相关文章进行学习。

前言

  1. 在1.8中ConcurrentHashMap仍然保留了segment,源码中注释中写道只是为了兼容以前版本的序列化而申明的类。
  2. 在1.8中ConcurrentHashMap由数组(Node)+链表+红黑树实现,与HashMap不同的是红黑树对象不是TreeNode,而是用TreeBin进行了封装。而在1.7中,ConcurrentHashMap采用锁分段技术,数组(Segment)+链表的数据结构。下图为1.7的ConcurrentHashMap的实现结构图。
  3. 1.7中的采用ReentrantLock+Segment+HashEntry,通过ReentrantLock来实现同步,到了1.8版本中synchronized +CAS +Node+红黑树,采用synchronized和大量的CAS操作实现同步和原子性操作。

jdk1.7实现同步和1.8实现同步的方式

  • 1.7的分段锁Segment继承于ReentrantLock,所以带有锁功能,保证线程安全,例如当执行put操作时,会进行2次hash,第一次hash定位到Segment位置,第二次定位到HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。此段描述基本来源于【JAVA秒会技术之ConcurrentHashMap】JDK1.7与JDK1.8源码区别

  • 前面已经提到,1.8中采用数组+链表+红黑树的数据结构来实现,这和1.8中HashMap的实现很相似,不同之处就是ConcurrentHashMap采用synchronized保证了线程安全以及使用了CAS来保证原子性操作。同样是对put举例,1.8中一次hash定位出数组的索引值table[i],接着使用synchronized来对table[i]进行锁定,保证线程安全,接着判断这个位置是否有元素,如果有,证明这个元素key以及存在,替换并返回旧值,如果为null,直接插入到table[i]这个位置,如果不为null,判断是链表或者红黑树,然后进行插入,这个过程基本和HashMap一致,不过这里的查找插入很多都是采用CAS的原子性操作。

//jdk1.7Segment定义
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile int count;   
    transient int modCount;          
    transient int threshold;       
    final float loadFactor;        
    transient volatile HashEntry<K,V>[] table;   
}

属性介绍

    // 最大的table容量2的30次方
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 数组的默认容量大小
    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;
    // 链表转红黑树阈值
    static final int TREEIFY_THRESHOLD = 8;
    // 红黑树转链表阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 在table数组容量小于64时,在链表大于8时也不会转红黑树,而是对数组进行扩容
    static final int MIN_TREEIFY_CAPACITY = 64;
    private static final int MIN_TRANSFER_STRIDE = 16;
    private static int RESIZE_STAMP_BITS = 16;
    // 最大线程数
    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;
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

    // 节点数组
    transient volatile Node<K,V>[] table;
    // 控制标识符
    /*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
     *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
     *当为0时:代表当时的table还没有被初始化
     *当为正数时:表示初始化或者下一次进行扩容的大小*/
    private transient volatile int sizeCtl;

Node

Node就是一个链表,可以指向下一个值,只允许查,不允许setValue()。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        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) {
            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;
        }
    }

TreeNode

TreeNode继承于Node,但是这是个红黑树的数据结构,在大于8时,Node链表便会转换为红黑树。

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,没找到返回null
        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;
        }
    }

TreeBin

可以看出TreeBin是将TreeNode红黑树进行了封装,增加了一些其他方法。

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for 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);
        }
        // 省略部分code...太多了
    }

put方法

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key如果为null抛出空指针异常
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash值,得到hashCode后再次hash散列
        int hash = spread(key.hashCode());
        // tab[i]位置的链表元素数量
        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();
            // 通过hash值与当前数组tab长度进行与,得到数组索引i以及f = 这个位置的Node元素,tabAt为CAS操作
            // 如果tab[i]位置为null,那么采用CAS操作创建新节点,将键值对插入tab[i],退出循环
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //检查table[i]的节点的hash是否等于MOVED,如果等于,则检测到正在扩容,则帮助其扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 走到这里说明tab[i]位置有元素,发生了碰撞,而且说明hash值不为MOVED
            else {
                V oldVal = null;
                // 对链表头结点进行锁定
                synchronized (f) {
                    // 再次进行重新检查
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            // 遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果找到这个节点已经存在,更新节点,退出循环
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                // 如果为null,创建节点,插入到队尾,退出循环
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 走到这里说明是树类型,调用TreeBin的putTreeVal进行替换或者插入节点操作
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 判断是否需要将链表转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    // 如果oldValue不为空,返回oldValue
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

注释很详细,肯定可以看懂,如果看着有些吃力的话建议先看一下HashMap的put源码HashMap源码学习笔记(jdk1.8),说白了,就是和HashMap的put方法实现几乎一样:

  • 都是先计算hash值
  • 接着定位数组tab索引
  • 判断索引位置元素是否是否为null,为null直接插入元素
  • 不为null,如果是链表,遍历链表,如果相同元素已经存在,替换并返回oldValue,如果不存在,那么将新元素插到链表尾部
  • 如果是树结构,调用树的相应方法进行操作。
  • 最后判断链表是否需要转为红黑树

不同之处在于用hash值定位到数组索引后,如果此位置元素为null,那么尝试CAS进行操作,如果不为空,而且判断正在扩容,那么多个线程帮助一起扩容,如果有这个位置有元素,而且没有在扩容,那么,ConcurrentHashMap为tab[i]进行了synchronized锁定,然后进行后续操作。

在putVal方法中涉及到table数组初始化方法spread()、initTable()、tabAt()、casTabAt()、helpTransfer()、treeifyBin()方法,接下来我们简单看一下具体实现。spread方法时计算hash值的方法,没什么好说的。

initTable方法

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // 当table为null时才进行初始化
        while ((tab = table) == null || tab.length == 0) {
            // 等sizeCtl小于0的时候表明正在初始化,sizeCtl初始默认状态为0
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // 线程从运行状态转到可运行状态进行等待
            // 否则将sizeCTL采用CAS操作设置为-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;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

tabAt()、casTabAt()方法

这三个CAS方法在ConcurrentHashMap的实现中用了很多,这是为了保证操作的原子性

    // 获取table索引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节点(将c和table[i]比较,相同则插入v)。  
    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);  
    }  
    // 设置节点位置的值,仅在上锁区被调用  
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {  
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);  
    }

 helpTransfer方法

在put方法中我们可以看出如果发现正在扩容,那么线程会调用helpTransfer()方法帮助一起扩容,这样效率会更高,也就是只要发现正在扩容,大家一起扩容,而不是等待正在扩容的线程进行扩容,当前线程等待扩容。

    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {//标志位为负数,扩容标志
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    // 进行扩容
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

transfer方法

这是方法才是进行并发扩容并将元素重新散列到新表的关键,方法很长,分析可以看这篇博客,分析的很详细并发编程——ConcurrentHashMap#transfer() 扩容逐行分析,在这里大概说一下并发扩容的关键:

  1. 将老表拆分,计算每个线程可以处理的桶区间。默认 16。
  2. 初始化临时变量 nextTable,扩容 2 倍。
  3. 死循环,计算下标。完成总体判断。
  4. 如果桶内有数据,同步转移数据。通常会像链表拆成 2 份,在新表中的下标为原索引或者或者原索引+原table长度。

get方法

get方法比较简单,思路就是计算hash值,定位到索引,如果是首节点就返回,如果遇到扩容,就调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回,如果都不是,那么继续往下遍历,匹配则返回。

    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());
        // 定位到table的索引位置的元素e
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果找到则返回
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果当前hash值<0,说明正在扩容,调用ForwardingNode的find方法来定位到nextTable来
            //查找,查找到就返回
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            // table[i]位置不为null,遍历查询
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

总结和思考

就像前面说的,1.7中采用的数据结构是Segment+HashEntry,1.8中采用是Node数组+链表+红黑树。对于线程安全,1.7采用reentrantlock,1.8采用synchronized和CAS。

  • 相对1.7来说,1.8降低了锁的粒度,1.7是对Segment加锁,而每个Segment下还有多个HashEntry,这样在访问一个HashEntry时,其他的就无法访问,在1.8中,采用对table数组头结点采用synchronized来实现同步,锁的粒度更低,效率更高。
  • 与1.8HashMap一样,同样采用数组+链表+红黑树,在hash碰撞严重的时候,大大提高了查询效率。
  • 1.7中必须进行2次hash,第一次hash定位到是哪个segment,第二次hash定位是哪个HashEntry。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。

   JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?

对于这个问题,这篇博客ConcurrentHashMap(JDK1.8)为什么要放弃Segment给出了以下解答

  1. 减少内存开销 假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
  2.  获得JVM的支持 可重入锁毕竟是API这个级别的,后续的性能优化空间很小。 synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

文章如有错误麻烦能够指出,谢谢

 

 

 

参考文献:

[1]https://blog.csdn.net/u010412719/article/details/52145145

[2]https://blog.csdn.net/qq296398300/article/details/79074239

[3]https://www.jianshu.com/p/2829fe36a8dd

[4]https://www.cnblogs.com/lijiasnong/p/9963808.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值