Java基础:ConcurrentHashMap

1.基于JDK1.7的实现

由一个Segment数组和多个HashEntry组成,而每一个Segment元素存储的是HashEntry数组+链表,即由数组 + 链表的结构实现

在这里插入图片描述
Segment
Segment本身就相当于一个HashMap对象,其中包含了一个HashEntry数组。
即,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
    
    // 快速失败(fail—fast)使用
    transient int modCount;
    
    // 大小
    transient int threshold;
    
    // 负载因子
    final float loadFactor;

}
1.1分段锁

Segment 是一种可重入锁(继承ReentrantLock)
每一个Segment 之间读写操作分离,互不影响
简单来说,就是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被其他线程访问

理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。即如果容量大小是16它的并发度就是16,可以同时允许16个线程操作16个Segment,而且还是线程安全的

例:不同Segment的并发写入
不同Segment的写入是可以并发执行的
在这里插入图片描述
例:同一Segment的一写一读
同一Segment的写和读是可以并发执行的
在这里插入图片描述
例:同一Segment的并发写入
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞
在这里插入图片描述

1.2put操作
 public V put(K key, V value) {
        Segment<K,V> s;
        //concurrentHashMap不允许key/value为空
        if (value == null)
            throw new NullPointerException();
        //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
        int hash = hash(key);
        //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
        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);
    }


tryLock() 是ReentrantLock获取锁一个方法。如果当前线程获取锁成功 返回true,如果别线程获取了锁返回false不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
    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,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        //遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value
                        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 {
                    	// 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
              //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列。扩容并rehash的这个过程是比较消耗资源的。
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

大致的流程:

  1. 计算出Key 通过Hash函数计算出hash值,再计算出当前key属于哪个Segment 然后调用Segment.put 分段方法Segment.put()
  2. Put 时候 ,通过Hash函数将即将要put 的元素均匀的放到所需要的Segment 段中,然后调用Segment的put 方法进行添加数据
  3. Segment的put 是加锁中完成的。如果当前元素数大于最大临界值的的话将会产生rehash. 先通过 getFirst 找到链表的表头部分,然后遍历链表,调用equals 比配是否存在相同的key ,如果找到的话,则将最新的Key 对应value值。如果没有找到,新增一个HashEntry 它加到整个Segment的头部。

简单来说就是:

  1. 为输入的Key做Hash运算,得到hash值
  2. 通过hash值,定位到对应的Segment对象
  3. 获取可重入锁
  4. 再次通过hash值,定位到Segment当中数组的具体位置
  5. 插入或覆盖HashEntry对象
  6. 释放锁
1.3get操作
//计算Segment中元素的数量
transient volatile int count;

    public V get(Object key) {  
        int hash = hash(key.hashCode());  
        return segmentFor(hash).get(key, hash);  
    }  


    final Segment<K,V> segmentFor(int hash) {  
        return segments[(hash >>> segmentShift) & segmentMask];  
    }  

    V get(Object key, int hash) {  
        if (count != 0) { // read-volatile  
            HashEntry<K,V> e = getFirst(hash);  
            while (e != null) {  
                if (e.hash == hash && key.equals(e.key)) {  
                    V v = e.value;  
                    if (v != null)  
                        return v;  
                    return readValueUnderLock(e); // recheck  
                }  
                e = e.next;  
            }  
        }  
        return null;  
    }  

大致过程:
将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值

整个过程没有加锁

简单来说:

  1. 为输入的Key做Hash运算,得到hash值
  2. 通过hash值,定位到对应的Segment对象
  3. 再次通过hash值,定位到Segment当中数组的具体位置
1.4size操作

问题:
在计算size的时候,还在并发的插入数据,可能会导致计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据)

解决方案:

  • 使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的
  • 如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回

2.JDK1.8的更新

①:抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
②:把HashEntry改成了Node,但是作用不变,把值和next采用了volatile修饰,保证了可见性
③:引入了红黑树,在链表大于 8 (默认值)的时候会转换

源码

2.1成员变量解析
  // node数组最大容量:2^30=1073741824  
  private  static  final  int  MAXIMUM_CAPACITY = 1  <<  30;  

  // 默认初始值,必须是2的幂数  
  private  static  final  int  DEFAULT_CAPACITY = 16;  

  //数组可能最大值,需要与toArray()相关方法关联  
  static  final  int  MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;  

  //并发级别,遗留下来的,为兼容以前的版本  
  private  static  final  int  DEFAULT_CONCURRENCY_LEVEL = 16;  

  // 负载因子,默认75%, 当table使用率达到75%时, 为减少table 的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh
  private  static  final  float  LOAD_FACTOR = 0.75f;  

  // 链表转红黑树阀值,> 8 链表转换为红黑树  
  static  final  int  TREEIFY_THRESHOLD = 8;  

  //树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))  
  static  final  int  UNTREEIFY_THRESHOLD = 6;  

  static  final  int  MIN_TREEIFY_CAPACITY = 64;  

  // 默认16, table扩容时, 每个线程最少迁移table的槽位个数
  private  static  final  int  MIN_TRANSFER_STRIDE = 16;  

  private  static  int  RESIZE_STAMP_BITS = 16;  

  // 2^15-1,help resize的最大线程数  
  private  static  final  int  MAX_RESIZERS = (1  << (32  -RESIZE_STAMP_BITS)) -  1;  

  // 32-16=16,sizeCtl中记录size大小的偏移量  
  private  static  final  int  RESIZE_STAMP_SHIFT = 32 -RESIZE_STAMP_BITS;  

  // forwarding nodes的hash值.值为-1, 当Node.hash为MOVED时, 代表着table正在扩容
  static  final  int  MOVED = -1;  

  // 树根节点的hash值.值为-2, 代表此元素后接红黑树
  static  final  int  TREEBIN = -2;  

  // ReservationNode的hash值  
  static  final  int  RESERVED  = -3;  

  // 可用处理器数量  
  static  final  int  NCPU = Runtime.getRuntime().availableProcessors();  

  //存放node的数组  
  transient  volatile  Node<K,V>[] table;  
  
  //控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义  
  //当为负数时:-1 代表正在初始化,-N代表有N-1个线程正在 进行扩容 
  //当为 0 时:代表当时的table还没有被初始化
  //当为正数时:表示初始化或者下一次进行扩容的大小
  private  transient  volatile  int  sizeCtl;  

  // table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable上
  private transient volatile Node<K,V>[] nextTable;

  // table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标
  private transient volatile int transferIndex;

  // 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为 ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就会帮着扩容
  static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
2.2put操作
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //根据 key 计算出hashcode值
        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)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //为当前key定位的Node,如果为空表示当前位置可以插入数据,利用CAS插入数据,失败则自旋用来保证成功
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前位置的hashcode == MOVED == -1,说明需要扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
               //如果都不满足,则利用synchronized写入数据
                V oldVal = null;
                synchronized (f) {
                    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) {
                            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) {
                    //如果数量>= TREEIFY_THRESHOLD,则转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
   

大致流程:

  1. 根据 key 计算出 hashcode
  2. 断是否需要进行初始化
  3. 当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
  5. 如果都不满足,则利用 synchronized 锁写入数据
  6. 如果数量大于等于 TREEIFY_THRESHOLD 则转换为红黑树
2.3get操作

        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        //根据计算出来的hashcode寻址
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来  
            //查找,查找到就返回  
            //如果是红黑树,则按照树的方式获取值
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //都不符合,则按照链表的方式遍历获取值
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //既不是首节点也不是ForwardingNode,那就往下遍历  
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    

大致步骤:

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

3.总结

  • JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  • 去除了分段锁,使得JDK1.8版本的数据结构变得更加简单
  • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个低效过程,而红黑树的遍历效率是很快的
  • 使用内置锁synchronized来代替重入锁ReentrantLock
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值