ConcurrentHashMap底层源码分析

上文讲到了hashmap的底层源码分析,可以了解到hashmap是线程不安全的。比如在场景多个线程同时调用put方法,会出现将前一个值给覆盖的现象。

 

 在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,主要实现原理是实现了锁分离的思路解决了多线程的安全问题。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是 HashEntry数组+链表,这个和HashMap的数据存储结构一样。 如下图所示:

                                   

 

为了保证线程安全,Segment继承了ReentrantLock。

 

 在构造方法中

    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }
复制代码
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;//左移,会出现1,2,4,8,16,32,64.。。,长度只会是幂次方
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;//
        if (c * ssize < initialCapacity)
            ++c;//为了防止不是整除,所以要+1,也是为了所有的HashEntry数组的长度大小一致
        int cap = MIN_SEGMENT_TABLE_CAPACITY;//cap最小为2,为了防止8/8,这样造成的HashEntry的数组长度都是为1,我们这个设置,这个每个最小是2
        while (cap < c)
            cap <<= 1;//左移,长度为2的幂次方
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);//对这个数组赋初始容量,可以将HashEntry看成hashMap,他的容量也要是2的幂次方的,都是为了计算index
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//构造Segment数组长度他并不是直接拿concurrencyLevel,这是因为这个和hashmap一样计算index是需要和长度-1进行与运算
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
复制代码

    从带参的构造方法中可以看出来,他有三个参数initialCapacity、loadFactor、concurrencyLevel,分别叫做初始容量、加载因子、并发级别。这里面的初始容量指的是Segment数组长度和HashEntry数组长度的总和;加载因子不多说;并发级别也可以叫做最大线程并发数量,他指的是Segment的长度,所以HashEntry的长度为initialCapacity/concurrencyLeve(真正其实是ssize)

 

 

复制代码
    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
复制代码

在进行put的时候,先判断segment数组中的第j个位置是否为空,如果为空就要创建HashEntry

复制代码
private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {//判断此时第j个位置是否还是为null(多个线程同时在j个位置put)
            Segment<K,V> proto = ss[0]; //从s[0]中直接取值(一些初始长度、加载因子、长度等),为了和s[0]的HashEntry保持一致
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//会再次进行判断,这第j个位置的HashEntry是否为空
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//再次判断是否为空
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//cas操作,是原子性操作,看第j个位置是否为null(期望值),为null就讲第j个位置赋HashEntry
                        break;
                }
            }
        }
        return seg;
    }
复制代码

当多个线程在创建Segment过程中,使用了多重判断以及cas来保证。

 

lock和trylock的区别:lock是阻塞加锁、trylock是非阻塞加锁

 

复制代码
 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//判断能否加到锁,不能加到锁则进入该方法,一直尝试加锁
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
复制代码
复制代码
 private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash); //获取到HashEntry第一个数组
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            while (!tryLock()) {  //while中一直尝试加锁
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);//非阻塞加锁其实是为了干更多的事,可以把HashEntry先构造出来
                        retries = 0;//表示已经存在该key的HashEntry
                    }
                    else if (key.equals(e.key))
                        retries = 0;//表示已经存在该key的HashEntry
                    else
                        e = e.next;//1.数组一直向下迭代,条件为下一个HashEntry不为空,且一直不存在相同的HashEntry
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&   //retries为双数的时候回进行判断一次,因为右边的判断是看  头节点是否发生了变化
                         (f = entryForHash(this, hash)) != first) { //使用的是头插法,如果头结点发生了变化,重新遍历数组
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }
复制代码

 

 

ConcurrentHashMap1.8源码分析

 

1.8中只有Node数组,没有Segment通过分段锁保持线程安全

复制代码
 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)//如果Node数组还未初始化
                tab = initTable();//进行初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//判断Node数组这个下标的是否存在Node
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))//通过cas来创建这个下标的Node
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//表示正在扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {//对第一个Node节点加锁
                    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;
                                }
                                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) {//如果是红黑数,HashMap中是TreeNode,TreeBin的好处就是加锁之后不会改变,因为里面包装了一个TreeNode节点
                            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);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
复制代码
复制代码
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; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
复制代码

 

posted @   WXY_WXY  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示