【ConcurrentHashMap 1.8】 理解性知识整理

451 阅读6分钟

1【理解】

ConcurrentHashMap:检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠,ConcurrentHashMap跟Hashtable类似但不同于HashMap,它不可以存放空值,key和value都不可以为null【null值用来判断是否需要加锁立即重试】。

ConcurrentHashMap从JDK1.5开始随java.util.concurrent包一起引入JDK中,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了

JDK1.8中的ConcurrentHashMap中仍然存在Segment这个类,而这个类的声明则是为了兼容之前的版本序列化而存在的。

JDK1.8中的ConcurrentHashMap不再使用Segment分段锁,而是以table数组的头结点作为synchronized的锁。和JDK1.8中的HashMap类似,对于hashCode相同的时候,在Node节点的数量少于8个时,这时的Node存储结构是链表形式,时间复杂度为O(N),当Node节点的个数超过8个时,则会转换为红黑树,此时访问的时间复杂度为O(long(N))。

2 【保证线程安全】

● 一、使用volatile保证当Node中的值变化时对于其他线程是可见的

【Node中的val和next都被volatile关键字修饰。我们改动val的值或者next的值对于其他线程是可见的,因为volatile关键字,会在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。】

【ConcurrentHashMap提供类似tabAt来读取Table数组中的元素,这里是以volatile读的方式读取table数组中的元素,主要通过Unsafe这个类来实现的,保证其他线程改变了这个数组中的值的情况下,在当前线程get的时候能拿到。】【】

【而与之对应的,是setTabAt,这里是以volatile写的方式往数组写入元素,这样能保证修改后能对其他线程可见。】

 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);

 }

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {

        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);

}

3 【使用table数组的头结点作为synchronized的锁来保证写操作的安全】

【当头结点不为null时,则使用该头结点加锁,这样就能多线程去put hashCode相同的时候不会出现数据丢失的问题。synchronized是互斥锁,有且只有一个线程能够拿到这个锁,从而保证了put操作是线程安全的。】

4 【当头结点为null时,使用CAS操作来保证数据能正确的写入】

    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();

//当头结点为null,则通过casTabAt方式写入

            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

            }

            else if ((fh = f.hash) == MOVED)

//正在扩容

                tab = helpTransfer(tab, f);

            else {

                V oldVal = null;

//头结点不为null,使用synchronized加锁

                synchronized (f) {

                    if (tabAt(tab, i) == f) {

                        if (fh >= 0) {

//此时hash桶是链表结构

                            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;

                                }

                                Node<K,V> pred = e;

                                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;

                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

                                    value)) != null) {

                                oldVal = p.val;

                                if (!onlyIfAbsent)

                                    p.val = value;

                            }

                        }

                        else if (f instanceof ReservationNode)

                            throw new IllegalStateException("Recursive update");

                    }

                }

                if (binCount != 0) {

//当链表结构大于等于8,则将链表转换为红黑树

                    if (binCount >= TREEIFY_THRESHOLD)

                        treeifyBin(tab, i);

                    if (oldVal != null)

                        return oldVal;

                    break;

                }

            }

        }

        addCount(1L, binCount);

        return null;

    }

【所谓的CAS,即compareAndSwap,执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。】

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);

}

【 asTabAt同样是通过调用Unsafe类来实现的,调用Unsafe的compareAndSwapObject来实现,其实如果仔细去追踪这条线路,会发现其实最终调用的是cmpxchg这个CPU指令来实现的,这是一个CPU的原子指令,能保证数据的一致性问题。】

1.8和之前的版本比较 参考 【ConcurrentHashMap 1.7】 理解性知识整 blog.csdn.net/qfzhangwei/…

旧版本的一个segment锁,保护了多个hash桶,而jdk8版本的一个锁只保护一个hash桶,由于锁的粒度变小了,写操作的并发性得到了极大的提升。

【更多的扩容线程】

扩容时,需要锁的保护。因此:旧版本最多可以同时扩容的线程数是segment锁的个数。

而jdk8的版本,理论上最多可以同时扩容的线程数是:hash桶的个数(table数组的长度)。但是为了防止扩容线程过多,ConcurrentHashMap规定了扩容线程每次最少迁移16个hash桶,因此jdk8的版本实际上最多可以同时扩容的线程数是:hash桶的个数/16,每个线程至少迁移16个桶。

【扩容期间,依然保证较高的并发度】

旧版本的segment锁,锁定范围太大,导致扩容期间,写并发度,严重下降。

而新版本的采用更加细粒度的hash桶级别锁,扩容期间,依然可以保证写操作的并发度。

【ConcurrentHashMap的重要结构与方法】

ConcurrentHashMap内部,和hashmap一样,维护了一个table数组,数组元素是Node链表或者红黑树.

【关于table数组,有3个重要方法】

//以volatile读的方式读取table数组中的元素

    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);

    }

//以volatile写的方式,将元素插入table数组

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {

        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);

    }

//以CAS的方式,将元素插入table数组

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,

                                        Node<K,V> c, Node<K,V> v) {

//原子的执行如下逻辑:如果tab[i]==c,则设置tab[i]=v,并返回ture.否则返回false

        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);

    }

旧版本对table数组元素的读写,都是在segment锁保护的情况下进行的,因此不会内存可见性问题。而jdk8的实现中,锁的粒度是hash桶,因此对table数组元素的读写,大部分都是在没有锁的保护下进行的,那么该如何保证table数组元素的内存可见性?【线程重试性代价小于----线程上下文交换的代价】 volatile

ConcurrentHashMap中的锁是hash桶的头结点,那么当多个put线程访问头结点为空的hash桶时,在没有互斥锁保护的情况下,多个put线程都会尝试将元素插入头结点,此时如何确保并发安全呢? CAS

6 【应用】

在哪里用的?
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }


然后,我们看下ConcurrentHashMap的put方法是如何通过CAS确保线程安全的:
假设此时有2个put线程,都发现此时桶为空,线程一执行casTabAt(tab,i,null,node1),此时tab[i]等于预期值null,因此会插入node1。随后线程二执行casTabAt(tba,i,null,node2),此时tab[i]不等于预期值null,插入失败。然后线程二会回到for循环开始处,重新获取tab[i]作为预期值,重复上述逻辑。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        ...
        for (Node<K,V>[] tab = table;;) {
            ...
            //key定位到的hash桶为空
            if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //cas设置tab[i]的头结点。
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;   //设置成功,跳出for循环
                //设置失败,说明tab[i]已经被另一个线程修改了。回到for循环开始处,重新判断hash桶是否为空。如何往复,直到设置成功,或者hash桶不空。
            }else{
               synchronized (f) {
                   //
               }
                
            }
        }
        ...
    }
CAS的其他应用
//JDK7版本的 AtomicInteger 类的原子自增操作
    public final int getAndIncrement() {
        for (;;) {
            //获取value
            int current = get();
            int next = current + 1;
            //value值没有变,说明其他线程没有自增过,将value设置为next
            if (compareAndSet(current, next))
                return current;
            //否则说明value值已经改变,回到循环开始处,重新获取value。
        }
    }

get方法
get方法同样利用了volatile特性,实现了无锁读。
查找value的过程如下:
1. 根据key定位hash桶,通过tabAt的volatile读,获取hash桶的头结点。
2. 通过头结点Node的volatile属性next,遍历Node链表
3. 找到目标node后,读取Node的volatile属性val
可见上述3个操作都是volatile读,因此可以做到在不加锁的情况下,保证value的内存可见性

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //定位目标hash桶,通过tabAt方法valatile读,读取hash桶的头结点
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //第一个节点就是要找的元素
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    //e.val也是valatile
                    return e.val;
            }
            //特殊节点(红黑树,已经迁移的节点(ForwardingNode)等
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //遍历node链表(e.next也是valitle变量)
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }


put方法
2. 由于锁的粒度是hash桶,多个put线程只有在请求同一个hash桶时,才会被阻塞。请求不同hash桶的put线程,可以并发执行。
4. put线程,请求的hash桶为空时,采用for循环+CAS的方式无锁插入。

remove方法
如图所示:删除的node节点的next依然指着下一个元素。此时若有一个遍历线程正在遍历这个已经删除的节点,这个遍历线程依然可以通过next属性访问下一个元素。从遍历线程的角度看,他并没有感知到此节点已经删除了,这说明了ConcurrentHashMap提供了弱一致性的迭代器。遍历操作可以参考ConcurrentHashMap源码分析(JDK8) 遍历操作分析





public V remove(Object key) {
        return replaceNode(key, null, null);
    }

    /**
    
       参数value:当 value==null 时 ,删除节点 。否则 更新节点的值为value
       
       参数cv:一个期望值, 当 map[key].value 等于期望值cv  或者 cv==null的时候 ,删除节点,或者更新节点的值
    */
    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //table还没有初始化或者key对应的hash桶为空
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            //正在扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    //cas获取tab[i],如果此时tab[i]!=f,说明其他线程修改了tab[i]。回到for循环开始处,重新执行
                    if (tabAt(tab, i) == f) {
                        //node链表
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                //找的key对应的node
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    //cv参数代表期望值
                                    //cv==null:表示直接更新value/删除节点
                                    //cv不为空,则只有在key的oldValue等于期望值的时候,才更新value/删除节点
                                    
                                    //符合更新value或者删除节点的条件
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        //更新value
                                        if (value != null)
                                            e.val = value;
                                        //删除非头节点
                                        else if (pred != null)
                                            pred.next = e.next;
                                        //删除头节点
                                        else
                                            //因为已经获取了头结点锁,所以此时不需要使用casTabAt
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                //当前节点不是目标节点,继续遍历下一个节点
                                pred = e;
                                if ((e = e.next) == null)
                                    //到达链表尾部,依旧没有找到,跳出循环
                                    break;
                            }
                        }
                        //红黑树
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        //如果删除了节点,更新size
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

7 【图示】