ConcurrentHashMap查询以及一些特殊方法

get

public V get(Object key) {
    // tab 当前集合的table的临时变量
    // e 数组中根据key 的hash值算出的索引位置的节点(以下统称 桶节点)
    // p 红黑树中找到的节点
    // eh 桶节点的hash值;ek 桶节点的key值
    Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
    // key 的hash值
    int h = spread(key.hashCode());
    // 判断集合数组是否为空,桶节点是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        // 数组不为空,且桶节点不为空,判断key引用是否与桶节点key引用ek是否一致或者equals一致
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                // key相同,返回桶节点
                return e.val;
        }
        // 桶节点是特殊节点:扩容中,数据在迁移;桶节点为占位节点;是红黑树
        else if (eh < 0)
        	// 桶节点时特殊节点时的查询find
            return (p = e.find(h, key)) != null ? p.val : null;
        // 桶节点下是链表,遍历链表(根据hash和key值比对,找到节点)
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 如果没有进入任何分支,说明集合中没有当前key值的节点,返回null
    return null;
}
桶节点是扩容时的数据迁移节点-find
static final class ForwardingNode<K, V> extends Node<K, V> {
    final Node<K, V>[] nextTable;
    // hash 值为MOVED = -1
    ForwardingNode(Node<K, V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }

    // 数据迁移时的find
    Node<K, V> find(int h, Object k) {
        // k get(key)中的key;h key的hash值;tab 扩容时新数组的临时变量
        outer: for (Node<K, V>[] tab = nextTable;;) {
            Node<K, V> e; int n;
            // e 新数组中根据h算出索引位置的节点的临时引用,n 新数组长度;
            // 如果k为空 ,或者新数组为null,或者新数组长度为0;或者索引位置的节点数据为空,直接返回null
            if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            // 上述条件都不满足,开始死循环
            for (;;) {
                // e的hash,e的key
                int eh; K ek;
                // 如果数组中的节点数据不为空,且hash值一致,并且key引用一致或者equals一致,说明要找的就是这个节点
                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;
                        // 不是扩容,数据迁移时,调用对应的finid,继续查找,(占用节点的find会直接返回null)
                    } else
                        return e.find(h, k);
                }
                // 上述情况都不满足,说明索引位置下是个链表,遍历链表,直到尾部
                if ((e = e.next) == null)
                    // 直到链表尾部都没找到,直接返回null
                    return null;
            }
        }
    }
}
桶节点是占位节点-find
    /**
     * 在compute和computeIfAbsent中会使用到占位节点 ReservationNode
     */
    static final class ReservationNode<K,V> extends Node<K,V> {
        ReservationNode() {
            super(RESERVED, null, null);
        }
		// 占位节点的find直接返回null
        Node<K,V> find(int h, Object k) {
            return null;
        }
    }
桶节点是红黑树节点-find
TreeBin-find

// 当前treebin的锁状态,默认0,表示线程没有上锁
volatile int lockState;
对锁状态进行运算的值
// 有现成正在写锁
static final int WRITER = 1;
// 有写线程,在等待获取写锁
static final int WAITER = 2;
// 读线程,在红黑树检索时,需要先对lockState + read
static final int READER = 4;

final Node<K, V> find(int h, Object k) {

        if (k != null) {
            // e:当前临时节点
            // 将当前临时节点指向双向链表头节点
            for (Node<K, V> e = first; e != null; ) {
                int s; K ek;
                /**
                 * s lockState 临时变量 ;
                 * WAITER 00000010
                 * |
                 * WRITER 00000001
                 * ------------------
                 *        00000011
                 * s & 00000011 != 0
                 * 说明当前有线程等待或持有写锁,从双向链表中查询
                 */
                if (((s = lockState) & (WAITER | WRITER)) != 0) {
                    // 判断双向链表头节点是否与key一致
                    if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        // 一致返回当前节点
                        return e;
                    // 不一致,遍历链表查找key相同的节点
                    e = e.next;
                    // 当前没有线程等待或持有写锁,CAS给 lockState +4,加读锁,读取红黑树
                } else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) {
                    TreeNode<K, V> r, p;
                    try {
                        // 基于findTreeNode在红黑树中检索数据
                        p = ((r = root) == null ? null : r.findTreeNode(h, k, null));
                    } finally {
                        // 会对lockState - 4,读线程拿到数据了,释放读锁
                        // 可以确认,如果-完4,等于WAITER | READ,说明有写线程可能在等待且当前线程为最后一个读线程,
                        // 判断waiter是否为null
                        Thread w;
                        if (U.getAndAddInt(this, LOCKSTATE, -READER) == (READER | WAITER) && (w = waiter) != null)
                            // 当前线程是最后一个在红黑树中检索的线程,同时有线程在等待持有写锁,唤醒等待的写线程
                            LockSupport.unpark(w);
                    }
                    return p;
                }
            }
        }
        return null;
    }

findTreeNode
        final TreeNode<K, V> findTreeNode(int h, Object k, Class<?> kc) {
            // h 查询key的hash值;k 查询key; kc 查询key 的类对象
            // 如果查询的key不为空
            if (k != null) {
                // 调用findTreeNode的节点-统称当前节点
                TreeNode<K, V> p = this;
                do  {
                    // ph 当前节点的hash值;dir = -1 查询左子树,dir = 1 查询右子树
                    // q 递归返回节点的接收值
                    // pl 当前节点的左子树;pr 当前节点的右子树

                    int ph, dir; K pk; TreeNode<K, V> q;
                    TreeNode<K, V> pl = p.left, pr = p.right;
                    // 如果当前节点的hash值 > 查询key的hash值,将当前节点的左子树指向当前节点
                    if ((ph = p.hash) > h)
                        p = pl;
                    // 如果当前节点的hash值 < 查询key的hash值,将当前节点的右子树指向当前节点
                    else if (ph < h)
                        p = pr;
                    // 如果当前节点的hash值 == 查询key的hash值,比较当前节点key与查询key是否一致,一致返回当前节点
                    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;
                    // 查询key的hash值与当前key的hash值一致,但是key不一致
                    else if ((kc != null ||
                              (kc = comparableClassFor(k)) != null) &&
                             (dir = compareComparables(kc, k, pk)) != 0)
                        // 进入判断,表示查询key的类对象不为空,当前类对象实现了ComparAble,重写了compareTo方法,
                        // 用compareTo决定循环查询左子树还是右子树
                        p = (dir < 0) ? pl : pr;
                    // 递归查询红黑树
                    else if ((q = pr.findTreeNode(h, k, kc)) != null)
                        return q;
                    // 循环查询左子树
                    else
                        p = pl;
                    // 遍历红黑树,直到当前节点为空
                } while (p != null);
            }
            return null;
        }
    }
特殊方法
compute

修改ConcurrentHashMap中指定key的value时,一般会选择先get出来,然后再拿到原value值,基于原value值做一些修改,最后再存放到咱们ConcurrentHashMap

整个流程和putVal方法很类似,但是内部涉及到了占位的情况RESERVED

整个compute方法和putVal的区别就是,compute方法的value需要计算,如果key存在,基于oldValue计算出新结果,如果key不存在,直接基于oldValue为null的情况,去计算新的value。

/**
 * 使用compute之前原value替换操作和使用compute之后原value替换操作
 */
private static void computeTest() {
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap();
    map.put("key", 1);
    // 修改key对应的value,追加上1

    // 之前的操作方式
    Integer oldValue = (Integer) map.get("key");
    Integer newValue = oldValue + 1;
    map.put("key", newValue);
    System.out.println(map);

    // 现在的操作方式
    map.compute("key", (key, computeOldValue) -> {
        if (computeOldValue == null) {
            computeOldValue = 0;
        }
        return computeOldValue + 1;
    });
    System.out.println(map);
}
compute源码解析
// compute 方法
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (key == null || remappingFunction == null)
        throw new NullPointerException();
    // 计算key的hash
    int h = spread(key.hashCode());
    V val = null;
    int delta = 0;
    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();
        // 桶上赋值
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            // 数组指定的索引位置是没有数据,当前数据必然要放到数组上。
            // 因为value需要计算得到,计算的时间不可估计,所以这里并没有通过CAS的方式处理并发操作,直接添加临时占用节点,
            // 并占用当前临时节点的锁资源。
            Node<K,V> r = new ReservationNode<K,V>();
            synchronized (r) {
                // 以CAS的方式将数据放上去
                if (casTabAt(tab, i, null, r)) {
                    binCount = 1;
                    Node<K,V> node = null;
                    try {
                        // 如果ReservationNode临时Node存放成功,直接开始计算value
                        if ((val = remappingFunction.apply(key, null)) != null) {
                            delta = 1;
                            // 将计算的value和传入的key封装成一个新Node,通过CAS存储到当前数组上
                            node = new Node<K,V>(h, key, val, null);
                        }
                    } finally {
                        setTabAt(tab, i, node);
                    }
                }
            }
            if (binCount != 0)
                break;
        }
        else {
            // 省略部分代码。主要是针对在链表上的替换、添加,以及在红黑树上的替换、添加
        }
    }
    if (delta != 0)
        addCount((long)delta, binCount);
    return val;
}
compute BUG

compute的BUG,如果在计算结果的函数中,又涉及到了当前的key,会造成死锁问题。

public static void main(String[] args) {
    ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap();

    map.compute("key",(k,v) -> {
        return map.compute("key",(key,value) -> {
            return 1111;
        });
    });
    System.out.println(map);
}
computeIfPresent和computeIfAbsent

computeIfPresent和computeIfAbsent其实就是将compute方法拆开成了两个方法

compute会在key不存在时,正常存放结果,如果key存在,就基于oldValue计算newValue

computeIfPresent:要求key在map中必须存在,需要基于oldValue计算newValue

computeIfAbsent:要求key在map中不能存在,必须为null,才会基于函数得到value存储进去

replace

涉及到类似CAS的操作,需要将ConcurrentHashMap的value从val1改为val2的场景就可以使用replace实现。

replace内部要求key必须存在,替换value值之前,要先比较oldValue,只有oldValue一致时,才会完成替换操作。

// replace方法调用的replaceNode方法, value:newValue,  cv:oldValue
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;
        // 在数组没有初始化时,或者key不存在时,什么都不干。
        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) {
                if (tabAt(tab, i) == f) {
                    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;
                                // 拿oldValue和原值做比较,如果一致,
                                if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
                                    // 可以开始替换
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        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) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

marge

merge(key,value,Function<oldValue,value>);

在使用merge时,有三种情况可能发生:

  • 如果key不存在,就跟put(key,value);
  • 如果key存在,就可以基于Function计算,得到最终结果
    • 结果不为null,将key对应的value,替换为Function的结果
    • 结果为null,删除当前key

分析merge源码

public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    if (key == null || value == null || remappingFunction == null) throw new NullPointerException();
    int h = spread(key.hashCode());
    V val = null;
    int delta = 0;
    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();
        // key不存在,直接执行正常的添加操作,将value作为值,添加到hashMap
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(h, key, value, null))) {
                delta = 1;
                val = value;
                break;
            }
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f, pred = null;; ++binCount) {
                            K ek;
                            // 判断链表中,有当前的key
                            if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                // 基于函数,计算value
                                val = remappingFunction.apply(e.val, value);
                                // 如果计算的value不为null,正常替换
                                if (val != null)
                                    e.val = val;
                                // 计算的value是null,直接让上一个指针指向我的next,绕过当前节点
                                else {
                                    delta = -1;
                                    Node<K,V> en = e.next;
                                    if (pred != null)
                                        pred.next = en;
                                    else
                                        setTabAt(tab, i, en);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null) {
                                delta = 1;
                                val = value;
                                pred.next =
                                    new Node<K,V>(h, key, val, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        binCount = 2;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r = t.root;
                        TreeNode<K,V> p = (r == null) ? null :
                            r.findTreeNode(h, key, null);
                        val = (p == null) ? value :
                            remappingFunction.apply(p.val, value);
                        if (val != null) {
                            if (p != null)
                                p.val = val;
                            else {
                                delta = 1;
                                t.putTreeVal(h, key, val);
                            }
                        }
                        else if (p != null) {
                            delta = -1;
                            if (t.removeTreeNode(p))
                                setTabAt(tab, i, untreeify(t.first));
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                break;
            }
        }
    }
    if (delta != 0)
        addCount((long)delta, binCount);
    return val;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ConcurrentHashMap的size方法返回当前映射中键值对的数量。由于ConcurrentHashMap是线程安全的,因此在多个线程同时访问时,size方法可能会返回一个近似值,而不是精确的值。这是因为在多个线程同时进行插入、删除等操作时,size方法需要遍历整个映射,而这个过程中可能会有其他线程同时进行修改操作,导致size方法返回的值不是最新的。因此,ConcurrentHashMap的size方法适用于需要一个近似值的场景,如果需要精确的值,可以使用其他方法,如遍历整个映射并计数。 ### 回答2: ConcurrentHashMap的size方法是用来获取当前ConcurrentHashMap中键值对的数量的。ConcurrentHashMapJava中的一个线程安全的哈希表,它通过细粒度的锁机制来实现多线程并发访问的效率。 在ConcurrentHashMap中,不同的线程可以同时进行读取操作,而且读取操作不需要加锁,这大大提高了读取操作的效率。但是如果有多个线程同时进行写入操作,那么这些写入操作就需要加锁来保证线程安全。 在ConcurrentHashMap的实现中,为了同时满足读取的高效率和写入的线程安全,ConcurrentHashMap使用了分段锁机制。它将整个哈希表分成了多个段(Segment),每个段都有自己的锁。这样不同的线程在写入操作时,只需要获取对应段的锁,而不会影响其他段的读取操作。这就保证了ConcurrentHashMap在并发场景下的高性能。 ConcurrentHashMap的size方法的原理是遍历整个哈希表,统计各个段中的键值对数量,并将其累加得到最终的结果。由于ConcurrentHashMap的读取操作不需要加锁,所以在统计的过程中,其他线程可以进行并发的读取操作。 需要注意的是,由于ConcurrentHashMap的size方法是通过遍历整个哈希表来统计数量的,所以在极端的高并发场景下,size方法的返回结果可能不是完全准确的。不过在大多数情况下,ConcurrentHashMap的size方法还是可以提供一个相对准确的结果的。 ### 回答3: ConcurrentHashMapJava中的一个线程安全的哈希表实现,它继承自HashMap并通过使用锁分段技术来实现高并发性能。 ConcurrentHashMap的size()方法用于获取当前哈希表中键值对的数量。该方法通过遍历哈希表的所有段(每个段都是一个小的哈希表),获取每个段中的键值对数量,并将这些数量累加得到最终的结果。由于ConcurrentHashMap的并发性能较高,所以在计算size()的过程中,其他线程可以继续执行其他操作,不会被阻塞。 在计算size()的过程中,如果有其他线程对哈希表进行插入、删除或更新操作,那么在计算结果时可能会发生一些变化。为了保证计算结果的准确性,ConcurrentHashMap使用了一些特殊的技术来解决并发冲突,如使用volatile修饰符保证可见性、使用CAS(Compare and Swap)操作来保证原子性等。 需要注意的是,由于ConcurrentHashMap的size()方法是基于估计值计算得到的,所以在某些情况下,它可能无法返回准确的结果。比如,在计算size()的同时,有其他线程不断地进行插入或删除操作,那么返回的结果可能比实际的键值对数量要大或小一些。如果对结果的准确性有较高的要求,可以考虑使用其他方式来获取准确的数量,例如迭代哈希表并计算实际存在的键值对数量。 总之,ConcurrentHashMap的size()方法是用于获取线程安全哈希表中键值对数量的方法,它通过分段的方式来保证高并发性能,并通过一些特殊的技术来解决并发冲突。但是需要注意的是,由于size()方法是基于估计值计算得到的,所以结果可能不是完全准确的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值