ConcurrentHashMap和LinkedHashMap的细致解析二合一

2023年 10月 16日 169.7k 0

作为并发版本的HashMap,它的很多特性都是和HashMap一样的, 那么在这里我主要还是从源码的层面解析一下,然后看看和HashMap有哪些不同的地方,同时怎么保证线程安全的。

ConcurrentHashMap解析

我在写的时候发现里面新增了很多属性,而且这些属性很晦涩,所以为了便于理解,我不会先解释属性的作用,还是从方法入手,然后逐步的扩展解释各种对比HashMap新增的属性的作用。

ConcurrentHashMap的构造方法解析

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity = (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               //这里传入参数和HashMap不同,HashMap是initialCapacity
               //主要就是考虑并发情况下table扩容的效率问题,比HashMap的初始容量要大
               //就是初始容量+初始容量向右移动一位+1 = 初始容量*1.5+1
               //空间更大,会降低一些扩容的几率,提高性能
               //tableSizeFor方法和Hash Map一样,都是传入参数最近的比它大的2的幂次倍
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

重头戏:putVal方法

这里面将会涉及到并发情况下的扩容处理,维持数据一致性问题,怕说起来没完,具体的扩容方法不再这里展示,还是2倍,但是实现细节都是针对多线程的情况,加了sychronized,我这里只循着主线看。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //这个计算Hash的方式和HashMap是不一样的,(h ^ (h >>> 16)) & HASH_BITS;
    //HASH_BITS这个值是0x7fffffff,转为二进制就是
    //2^31-1,二进制就是首位0,其他的都是1。所以这样的计算结果就会发现
    //如果原hashcode是负数,那么就会将最高位设置成0,也就是正数,然后
    //其他位保持不变,负数在和长度-1进行&运算的时候会和正数有差异
    //会导致向左移动,在并发的环境下更容易造成hash冲突。所以加了这样一个变化
    int hash = spread(key.hashCode());
    int binCount = 0;
    //循环table
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        //如果table为空,就初始化,然后直接转移到下面看初始化的方法------------------1,做个标记一会回来
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //长度-1 &hash计算索引地址然后调用tabAt方法一行代码,信息量很大
        //Unsafe包的getObjectVolatile方法,防止指令重排弄乱结果
        //(Node)U.getObjectVolatile(tab, ((long)i  k = ConcurrentHashMap.class;
        //    SIZECTL = U.objectFieldOffset
        //        (k.getDeclaredField("sizeCtl"));
        //所以到这里我们可以猜测一下,sizeCtl 是干啥的,首先它是volatile的,那么它一定是独立线程之外的东西,去记载某些东西
        //我们知道多线程中有一个ctl,来记录线程池状态的,那么这个sizeCtl是不是也是和记录状态相关,和size挂上,那么是不是和扩容状态相关?
        //我们往下找推测(虽然源码给注释了,但还是想自己推测一下作用,记忆深刻)
        //compareAndSwapInt这个方法的第二个参数是内存偏移量,而这个常量确实是sizeCtl字段的内存偏移量
        //所以这个方法就是去SIZECTL的内存偏移量找数据,然后和sc对比,
        //如果相等 ,然后就将SIZECTL偏移量位置的数据,也就是sizeCtl设置为-1,返回true,
        //否则不动,返回false,就是CAS操作,所以sizeCtl =-1的时候说明table正在初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //如果table为空的时候
                if ((tab = table) == null || tab.length == 0) {
                    //如果sc>0,sc给n,否则n设置为默认容量16
                    //而从上面的构造方法中我们发现sc经过计算是>0的,而有一种情况,就是调用ConcurrentHashMap的无参构造方法
                    //这个时候就是0了,然后就取默认容量了,然后创建个这个长度的数组,给table,table就初始化好了
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node[] nt = (Node[])new Node[n];
                    table = tab = nt;
                    //然后sc设置为数组长度的四分之三,也就是0.75。嗯?变成了扩容的阈值了
                    sc = n - (n >>> 2);
                }
            } finally {
                //这个时候将sizeCtl赋值为sc,这个方法是初始化table,如果刚开始都是0的时候sizeCtl就是扩容阈值了,同时SIZECTL = -1
                //如果不是的话,sizeCtl,就取sc的值
                sizeCtl = sc;
            }
            //回去接着看putVal
            break;
        }
    }
    return tab;
}

这个初始化的方法中sizeCtl我们可以看出,如果它>0的时候就表示已经初始化过了,等于阈值。如果=0的时候那么就是还没有初始化,等于-1的时候就是正在初始化。这一下出来三种状态的表示了,还有一状态在putVal注释中写了。

小总结:: 虽然我没有一行一行代码的进行分析,只分析了一些我们没有在HashMap中见到的代码,从这些代码中我们可以发现ConcurrentHashMap能保证并发下的数据一致性就是依赖于锁,也就是sychronized,而且细化到了节点,摒弃了1.7中分段锁的概念,很大程度的提升了性能,节省了锁的开销,同时用大量的CAS操作来保证数据的一致性,而扩容的算法,计算index等都是相同的算法。比如里面有个非常好的思路,如果你查找数据时候发现正在扩容,那么就帮助扩容,自己将自己需要的数据移动到新的数组中,然后返回。get方法我就不列出来了,因为很多代码都和putVal重复,毕竟更新也许需要先查找的,大家可以自己看一看。对于并发容器最核心的是对多线层的理解,后面我会出文章仔细分析Java的多线程和相关的池化技术。

LinkedHashMap解析

在这里我可以简单的说一下,不管是ConcurrentHashMap还是LinkedHashMap其实底层的思想都是HashMap,弄懂HashMap,那么这两个容器对我们来说都是进行扩展,只不过在展现形式上不同。大家可以看我的这篇文章# HashMap很细的分析--令人发指,比如ConcurrentHashMap完善了多线程环境下的数据一致性问题,而LinkedHashMap也有它的优势,比如它可以按照数据插入的顺序进行访问,支持按照元素的访问顺序进行排序(可以作为LRU容器)等。那么我也是和上面一样,从源码的角度分析一下LinkedHashMap

LinkedHashMap的结构

相对于HashMap,它的结构是一个双向链表,我们上面的特性说过了,它支持按照插入顺序进行遍历,但是呢HashMap本身是不支持的,所以就需要扩展存储数据的节点,而它的双向链表就体现出来了,看下面它的结构,它继承了Node,然后定义了beforeafter,这样就能使链表具备双向的能力了。

static class Entry extends HashMap.Node {
    Entry before, after;
    Entry(int hash, K key, V value, Node next) {
        super(hash, key, value, next);
    }
}

LinkedHashMap get方法解析

相对于ConcurrentHashMapLinkedHashMap真的是非常简单的,连Put方法都是使用父类HashMap的,那么我们看一下get方法的源码

public V get(Object key) {
    Node e;
    //直接调用了HashMap的getNode方法,脸都不要了
    if ((e = getNode(hash(key), key)) == null)
        return null;
    //这里我们猜都能猜出来是干啥的,accessOrder就是访问顺序的意思,也就是如果你的LinedHash
    //是支持访问顺序排序的话,那么这个参数就是true,那我们也就知道,这个方法就是维护访问的
    //作用就是将访问的元素移动到链表末尾
    //因为是尾插法,按照插入顺序访问就直接从尾开始,这就会有疑问,每个桶都有链表
    //咋能知道拿个链表的尾部是真正最后插入的数据节点呢?看下面的方法
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

LinkedHashMap重写了newNode的方法,在linkNodeLast方法为Entry指定了beforeafter

Node newNode(int hash, K key, V value, Node e) {
    LinkedHashMap.Entry p =
        new LinkedHashMap.Entry(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
private void linkNodeLast(LinkedHashMap.Entry p) {
    //因为插入的都在链表的尾部
    LinkedHashMap.Entry last = tail;
    tail = p;
    //如果尾部位空,说明是个新节点,before和after都没有,毕竟就自己一个节点
    if (last == null)
        head = p;
    else {
        //否则尾部的after就是新节点,新节点的before就是尾部,双向了吧
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap对于插入和删除节点时的维护解析

如果看过我写HashMap解析的时候应该看到我在put方法中有一个方法的注释写的是HashMap是空实现,LinkedHashMap会使用,这个方法就是afterNodeInsertion,插入节点的后处理方法,这个就是个钩子方法,也叫做模版方法。

afterNodeInsertion方法解析

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry first;
    //removeEldestEntry如果自己实现LRU的时候需要重写,就是逐出数据的条件
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        //移除头节点
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

afterNodeRemoval方法解析

这个也是模版方法,在HashMap移除节点的时候调用,看注释吧

void afterNodeRemoval(Node e) { // unlink
    LinkedHashMap.Entry p =
        (LinkedHashMap.Entry)e, b = p.before, a = p.after;
     //把p的before和after都设置null,断开联系
    p.before = p.after = null;
    //下面就是干一件事,删除节点之后得前后连起来。
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论