ConcurrentHashMap的get、put方法分析

面试常问道会问到JUC,常见的就是CHM了,来说一下CHM不同版本的方法操作

JDK7的CHM的底层实现是分段segment数组实现的,segment里面包含HashEntry,每个HashEntry又组成一个链表。

常用方法get方法:

  • 首先获取segment的锁;
  • 然后进行根据Hash算法定位到具体的桶的位置;
  • 如果桶不为空的话,并且根据判断第一个结点就是需要插入的位置,直接替换旧值;
  • 否则的话逐个进行遍历寻找,不存在冲突的话创建一个新结点进行头插,
  • 如果桶为空的话,进行创建结点直接插入;
  • 之后进行判断当前结点数是否超过限制值,超过进行扩容
  • 然后进行基数+1,
  • 最后需要进行释放锁,因为segment继承Lock锁,防止阻塞产生。
//这是Segment中的 put 方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//此处实现需要回去segment的锁
 HashEntry<K,V> node = tryLock() ? null :
  scanAndLockForPut(key, hash, value);
 V oldValue;
 try {
  //当前Segment的table数组
  HashEntry<K,V>[] tab = table;
  //进行Hash计算,找到其所在HashEntry数组的下标
  int index = (tab.length - 1) & hash;
  //当前下标位置的第一个HashEntry节点
  HashEntry<K,V> first = entryAt(tab, index);
  for (HashEntry<K,V> e = first;;) {
   //如果第一个节点不为空
   if (e != null) {
    K k;
    //如果第一个节点,就是要插入的节点,则替换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;
   }
   //如果当前位置没有节点
   //或者当前链表已经遍历完了还没找到相等的key
   else {
    //如果不为空,则直接头插
    if (node != null)
     node.setNext(first);
    //否则,创建一个新的结点,并头插
    else
     node = new HashEntry<K,V>(hash, key, value, first);
    int c = count + 1;
    //如果当前Segment中的元素大于阈值,并且tab长度没有超过容量最大值,则扩容
    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
     rehash(node);
    else
     setEntryAt(tab, index, node);
    ++modCount;
    //更新count值
    count = c;
    oldValue = null;
    break;
   }
  }
 } finally {
  //ReentrantLock必须手动解锁
  unlock();
	 }
 return oldValue;
	}
}

get方法:
相对就很简单

  • 根据Hash计算出segment的位置
  • 判断是否为空,判断链表是否为空,然后进行遍历链表
  • 根据查找到返回,没有返回null即可

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

public V get(Object key) {
 Segment<K,V> s;
 HashEntry<K,V>[] tab;
 //计算hash值
 int h = hash(key);
 //先定位到 key 所在的Segment
 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
 if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
  (tab = s.table) != null) {
  //若Segment不为空,且链表也不为空,则遍历查找节点
  for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
    e != null; e = e.next) {
   K k;
   //找到则返回它的 value 值,否则返回 null
   if ((k = e.key) == key || (e.hash == h && key.equals(k)))
    return e.value;
  }
 }
 return null;
}

JDK8的常用方法put

在JDK8的CHM已经变得和原来不同了,数据结构实现是Node数组+链表+红黑树进行实现,没有再使用分段锁,并发而是使用优化后的synchronized+CAS。

put方法的步骤:

final V putVal(K key, V value, boolean onlyIfAbsent) {
		//首先可以看见的就是不允许存放null值
        if (key == null || value == null) throw new NullPointerException();
        //计算Hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //判断CHM是否为空  
            if (tab == null || (n = tab.length) == 0)
            	//只有一个线程可以进行初始化
                tab = initTable();
                //若已经初始化,根据Hash计算index,看table[index]是否为空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//为空的话,进行CAS插入一个新的结点,并且只有一个结点成功
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                 
            }
            //桶不为空,判断结点Hash是否为MOVDE(-1),是否进行数组扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //否则进行加锁保证线程安全
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            //进行遍历,寻找是否有产生冲突key的结点,则直接替换结点值
                            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) {
                	//判断结点数是否大于等于8  否则进行红黑树转换
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //无异常的话进行结点数增加
        addCount(1L, binCount);
        return null;
    }

get方法:

  • 根据key计算Hash值
  • table不为空且找到具体的桶,进行遍历查找
  • 否则进行树的遍历
  • 否则进行链表进行遍历查询 有就返回,否则返回null
public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        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;
            }
            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;
    }

再有就是JDK7计算size的方式:

  • 使用乐观的方式进行计算,假定计算的时候没有修改segment结构的操作,但是如果发生了就进行重试,两次重试失败的话就会进行锁住所有的segment,然后再进行计算size,已确定准确的大小。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值