ConcurrentHashMap

一、ConcurrentHashMap1.7

1、背景

  传统HashTable保证线程安全,采用synchronized锁将整个HashTable中的数组锁住,在多个线程中只允许一个线程访问Put或者Get,效率非常低。

  JDK官方不推荐在多线程的情况下使用HashTable或者HashMap,建议使用ConcurrentHashMap分段HashMap,能保证效率和安全性。

2、ConcurrentHashMap1.7架构

       数据结构数组+Segments分段锁+HashEntry链表
       锁的实现:Lock锁+CAS乐观锁+UNSAFE类
       扩容:支持多个Segment同时扩容。

   默认有16个不同的Segment,每个Segment中都有自己独立的HashEntry<K,V>[] table。
  第一次计算存放在哪个Segment对象中,第二次计算Segment对象中哪个HashEntry<K,V>[] table下标位置。
       因为有两个数组,一个是Segment[],一个是HashEntry[],jdk7会对每一个HashEntry数组上锁。 

 

3、核心参数

##1.无参构造函数分析:
initialCapacity ---16 
loadFactor  HashEntry<K,V>[] table; 加载因子0.75
concurrencyLevel 并发级别 默认是16
##2. 并发级别是能够大于2的16次方
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
##3.sshift 左移位的次数 ssize 作用:记录segment数组大小
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
##4. segmentShift segmentMask:ssize - 1 做与运算的时候能够将key均匀存放;
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
##5. 初始化Segment0 赋值为下标0的位置
Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);    
##6.采用CAS修改复制给Segment数组
 UNSAFE.putOrderedObject(ss, SBASE, s0); // or     

4、Put方法

Put方法底层的实现  简单分析
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        ###计算key存放那个Segment数组下标位置;
        int hash = hash(key);
        int j = (hash >>> segmentShift28) & segmentMask15;
//保留最高4位与15做与运算
        ###使用cas 获取Segment[10]对象 如果没有获取到的情况下,则创建一个新的segment对象
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        ### 使用lock锁对put方法保证线程安全问题
        return s.put(key, hash, value, false);
0000 0000 00000 0000 0000 0000 0000 0011
                                    0000 0000 00000 0000 0000 0000 0000 0011

5、深度分析

Segment<K,V> ensureSegment(int k) 
  
    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;
        ### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            ## 使用原型模式 将下标为0的Segment设定参数信息 赋值到新的Segment对象中
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            #### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                ###创建一个新的Segment对象
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    ###使用CAS做修改
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
}

   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;
                ###计算该key存放的index下标位置
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        ###查找链表如果链表中存在该key的则修改
                        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结点 头插入法
                            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;
        }

二、ConcurrentHashMap1.8

1、背景

        ConcurrentHashMap1.7需要计算两次index值,并且Segments分段锁效率也不是很高,所以ConcurrentHashMap1.8取消分段锁,采用数组+链表+红黑树数据结构,Node节点保存数据,并基于CAS + synchronized保证node节点数据安全,将锁的粒度拆分到每个index。

  数据结构:数组+链表+红黑树;直接使用Node节点来保存数据。
       锁的实现:基于CAS + synchronized保证node节点数据安全,将锁的粒度拆分到每个index。
  锁的竞争:多个线程同时put key的时候,多个key都落在同一个index node节点时,如果index没有发生冲突,基于CAS操作,如果发生冲突,则基于Synchronized操作。

 2、源码分析

  1)构造函数为空,说明是懒加载;

  2)不支持key为null(put方法中)

  3)binCount 记录链表长度,如果大于8的情况下,则链表转红黑树(数组容量≥64)。

  4)在全局共享变量中加上volatile关键字,及时读取最新主内存数据,保证线程可见性(为第6点准备)。

  5)sizeCtl  默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。-1 代表一个线程正在进行table初始化扩容,-2 代表两个线程正在进行table初始化扩容(为第6点准备)。

  6)ConcurrentHashMap1.8 ,每个线程做put操作初始化时,发现sizeCtl <0会进行自旋状态,非常消耗cpu,导致cpu飙升。当一个线程获取到锁至给table初始化的整个阶段,其他线程都一直在做自旋,直到判断table不为null才退出,如果占用资源的线程在给table赋值之前断电,其他线程一直自旋。

  7)初始化默认长度16

  8)sc 提前扩容的元素数量  sc = n - (n>>>2);

  9)CAS使用:多个线程同时赋值修改没有冲突的index位置元素时使用CAS(对比第11点)

        如果CAS修改成功,则直接退出自旋,否在继续自旋。

  10)并发扩容时做辅助扩容,非常厉害     

  11)Synchronized使用 :多个线程同时赋值修改index位置冲突元素时使用Synchronized锁(对比第9点)

  12)put时会对相应节点的链表进行扫描并判断,如果key相等则修改,不相等则找到最后一个元素,使用尾插法添加。

  13)addCount  提前扩容;对size做++.CounterCells。记录每个线程size++的次数。

  14)如何统计size?

        ConcurrentHashMap1.7 每个segment有独立统计size值,可以通过累加每个segment中的size。

        ConcurrentHashMap1.8,每个线程中有增加元素时,会该线程随机数与length-1取余,得出在CounterCells数组(多线程可见)中index下标位置,对其下标元素+1,最后对所有下标元素值累加得出size。当线程随机数取余产生index冲突,且多线程对该index操作时,使用cas进行操作。

3、为什么ConcurrentHashMap1.8使用Synchronized锁而不是用Lock锁?

Synchronized锁自带自旋功能,而且有锁的升级过程; Lock都没有。

4、ConcurrentHashMap1.7和1.8的区别?

数据结构:1.7数组+Segments分段锁+HashEntry链表;1.8数组+链表+红黑树(直接使用node存储数据)

锁的实现:1.7Lock锁+CAS + UNSAFE;1.8index没有发生冲突使用cas,发生冲突则使用Synchronized锁。

③ 扩容实现:1.7支持多个Segment同时扩容;1.8支持并发扩容。

5、ConcurrentHashMap为什么在构造函数初始化s0?

        S0相当于模板,其他key落到其他segment上时,以S0的参数为模板初始化segment。  

6、ConcurrentHashMap 底层是如何实现?

1.传统方式 使用 HashTable 保证线程问题,是采用synchronized 锁将整个HashTable 中的数组锁住, 在多个线程中只允许一个线程访问 Put 或者 Get,效率非常低,但是能够保证线程安全问题。
2.多线程的情况下 JDK 官方推荐使用 ConcurrentHashMap ConcurrentHashMap 1.7 采用分段锁设计 底层实现原理:数组+Segments 分段锁+HashEntry 链表实现 大致原理就是将一个大的 HashMap 分成 n 多个不同的小的HashTable ,不同的 key 计算 index 如果没有发生冲突 则存放到不同的小的HashTable 中,从而可以实现多线程,同时做 put 操作,但是如果多个线程同时put 操作key 发生了 index 冲突落到同一个小的 HashTable 中还是会发生竞争锁。
3.ConcurrentHashMap 1.7 采用 Lock 锁+CAS 乐观锁+UNSAFE 类里面有实现类似于 synchronized 锁的升级过程。
4.ConcurrentHashMap 1.8 版本 put 操作 取消 segment 分段设计直接使用Node数组来保存数据 index 没有发生冲突使用 cas 锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值