Java并发集合之ConcurrentHashMap源码解析

目录

1.ConcurrentHashMap

1.1整体框架

1.2put方法源码解析

1.3数组初始化时的源码解析

1.4扩容的源码分析

1.5get方法源码解析


1.ConcurrentHashMap


1.1整体框架

  • HashTable是对实例方法进行加锁,会锁住整个实例对象,
  • 1.7中ConcurrentHashMap使用了分段锁,对每个段进行加锁,降低了锁的粒度
  • 1.8中摒弃了分段锁对具体的槽点进行加锁并且配合CAS无锁操作,共同实现了更加细粒度的锁定
  • 1.8中新增了转移节点,保证扩容时候的线程安全
  • get方法不需要加锁,因为使用了volatil关键字进行了修饰
static final class HashEntry {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry next;
    ...
}

1.2put方法源码解析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //计算hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    //整个的循环
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        //table是空的,进行初始化数组
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //如果当前索引位置没有值,直接CAS创建
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))
                break;
        }
        //如果当前槽点是转移节点,表示该节点已经重哈希到新数组上了不能修改,然后就一直等待扩容完成
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //槽点上有值的
        else {
            V oldVal = null;
            //锁定当前槽点,其余线程不能操作,保证了安全
            synchronized (f) {
                //从赋值到安全点这个过程不安全,需要在判断 i 索引位置的数据没有被修改
                //binCount 被赋值的话,说明走到了修改表的过程里面
                if (tabAt(tab, i) == f) {
                    //链表
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node 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 pred = e;
                            // 没有重复值就会到达链表尾部,直接新增
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //红黑树TreeBin 需要对根节点加锁,槽点不一定是根节点所以需要再次加锁
                    else if (f instanceof TreeBin) {
                        Node p;
                        binCount = 2;
                        // 如果有重复的值会判断,是否进行更新
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了
            if (binCount != 0) {
                // 链表是否需要转化成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                // 新增失败才会走到这里,基本不会走到这里
                break;
            }
        }
    }
    //check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法
    addCount(1L, binCount);
    return null;
}

计算哈希值找到对应的槽点,如果数组为空会进行初始化,如果槽点为空(没有哈希冲突)通过CAS进行设置值,如果槽点是转移节点会进行等待扩容完成在进行

如果哈希出现了冲突,首先对槽点进行加锁,然后在判断一下槽点的期望值是否符合当前值,满足后:

如果是链表那么就会进行遍历,如果遇到重复值就会判断是否进行更新,否则就会在尾部新增节点

如果是红黑树那么需要对根节点进行加锁(因为槽点不一定是根节点),也会进行重复值的更新是否进行判断

最后,判断是否需要链表转化为红黑树,如果有只是更新了值那么直接返回,最终判断是否需要进行扩容。

1.3数组初始化时的源码解析

//初始化 table,通过对 sizeCtl 的变量赋值来保证数组只能被初始化一次
private final Node[] initTable() {
    Node[] tab; int sc;
    //通过自旋保证初始化成功
    while ((tab = table) == null || tab.length == 0) {
        // 小于 0 代表有线程正在初始化,需要释放当前 CPU 的调度权
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        // CAS 原子操作通过变量赋值,让线程可以阻塞在上面的if判断
        // 但是,可能线程1已经运行过上面的if了,运行在这里然后切换了
        // 直到线程2运行完下面的所有操作,线程1开始恢复进入了CAS操作
        // 所以需要再次进行判断
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 很有可能执行到这里的时候,table 已经不为空了,这里是双重 check
                if ((tab = table) == null || tab.length == 0) {
                    // 进行初始化
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node[] nt = (Node[])new Node[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

初始化操作没有通过锁来保证安全性,而是通过复杂的CAS设置变量,来进行阻塞,并且加入双重判断来保证安全性,

ps:第二个判断,主要还是因为可能有多个线程执行完了第一个if,都进入到了CAS操作

1.4扩容的源码分析

//addCount方法的部分,保证了初始化的正确性
int rs = resizeStamp(n);
if (sc < 0) {
    if ((nt = nextTable) == null || transferIndex <= 0)
        break;
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc - 1))
        transfer(tab, nt);

太长太复杂了,简单说下思路吧!

首先创建数组(新数组为老数组大小的两倍),也是通过CAS设置变量值+双重判断来进行初始化的

然后从后向前拷贝旧数组的槽点,拷贝的时候会先把槽点锁住(链表就不需要再加锁了和红黑树还需要在根节点加锁),拷贝完成后会设置为转移节点,就不能在进行更改了直到扩容完成。

1.5get方法源码解析

public V get(Object key) {
    Node[] tab; Node e, p; int n, eh; K ek;
    //计算hashcode
    int h = spread(key.hashCode());
    //不是空的数组 && 并且当前索引的槽点数据不是空的
    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;
        }
        // 如果是红黑树或者转移节点,使用对应的find方法
        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;
}

首先计算出索引值,判断hashcode值在判断key是否相等,如果相等直接返回,否则一定出现了哈希冲突

链表进行查找就是从前向后进行遍历,

红黑树查找就是二叉排序树进行查找,通过比较大小判断是在左子树还是在右子树上,

你可能感兴趣的:(JDK源码分析,java,面试)