ConcurrentHashMap中的同步

由于HashMap是非线程安全的,扩容时容易造成环形链表,导致在获取链表中不存在的值时陷入死循环。于是在HashMap在并发环境中需要加锁使用,这种方式开销大。Java中提供了另一种数据结构ConcurrentHashMap,专门用于并发环境下的Map映射需求。

具体机制:CAS 结合 synchronized,并通过sun.misc.Unsafe提供的可见性操作接口来访问数据。

主要关注以下几个地方:

  • put 放数据
  • resize 扩容
  • get 取数据

1、对于 put 操作

key值求hash后所在table中索引位置无节点(null),通过CAS方式将节点插入到该索引位置
key值求hash后所在table中索引位置有节点(不为null),通过synchronized锁住该索引位置处链表或者红黑树的第一个节点

2、对于 resize 扩容操作

多线程都能参与扩容操作,多线程同步怎么做的?

1)sizeCtl :该标签用于保证resize调整不会重叠,在初始化和resize扩容的时候,sizeCtl负值。其中 -1:表示table正在初始化 -(1+活跃的扩容线程数):表示正在扩容。其它也有默认值、初始化表时的表的size值、或者下一次扩容阈值。

每个线程在帮助扩容时,都会通过CAS来对sizeCtl进行+1操作,表示增加了一个线程帮助扩容

2)ForwardingNode:标记节点,用来进行并发控制,对旧table中标记为ForwardingNode的节点不再处理,避免重复。

3)transferIndex:扩容时旧table中下一个需要转移的一个长度单位的起始下标,不管哪个线程一次任务中就处理 MIN_TRANSFER_STRIDE 这么多个。

先来看看怎么确定某个元素put后到达table中的下标位置

i = (n - 1) & hash

n 表示 table 长度,hash 表示插入元素的哈希值,这么做是因为 n 是 2 的幂,n-1 其二进制表示中各位均为1。
(n - 1) & hash 与 hash % n 取余的效果相同。这样便将 hash 值分布到 table 中。

transfer中有一段代码不好理解,这里是将旧表中的元素移到新表中去

if (fh >= 0) {
  // fh & (n - 1) 决定了f节点在table中的下标,因为n是2的幂,
  // 所以 fh & n 只有两个结果,要么是 0,要么是 n
  // 所以这里是为了将该链表拆分成两个部分,结果为0的为一部分,为n的为另一部分
  // 扩容之后数组长度增加一倍,数组元素要重新分布
  // 最好的情况当然是原长度部分和扩容长度部分的元素各一半,并且尽多保持原来的顺序
  int runBit = fh & n;
  Node<K,V> lastRun = f;
  // 从链表的第一个节点开始,遍历链表
  for (Node<K,V> p = f.next; p != null; p = p.next) {
  	  // 前边说过 p.hash & n 要么为0,要么为n
  	  // 所以b的结果可能会出现很过个..0.0..n.n..0..n.0这样的情况
      int b = p.hash & n;
      if (b != runBit) {
      	  // 于是 lastRun 便记录下了最后一个b值发生变化的节点
      	  // 这样的目的在于,将放置到扩容长度部分中去的元素的链表从lastRun节点开始
      	  // 也就是从后往前(头插方式)
      	  // 还有一点需要关注,如果连续node的b值均为n或者0,那这连续的几个节点是不用一个一个搬运的
      	  // 因为他们在新table中的位置是一样的,并且其next关系不变
      	  // 所以搬运时只需要搬这几个连续节点中的第一个就行了
      	  // 但是从后文看并不是这样的,后边只是对从lastRun节点开始的连续节点不用全部遍历,
      	  // 其他节点还是一个一个处理的,那假如lastRun节点之前有好长一串其b值都连续相同,
      	  // 那一个一个处理岂不是比较愚蠢?
          runBit = b;
          lastRun = p;
      }
  }
  if (runBit == 0) {
      ln = lastRun;
      hn = null;
  }
  else {
      hn = lastRun;
      ln = null;
  }
  for (Node<K,V> p = f; p != lastRun; p = p.next) {
      int ph = p.hash; K pk = p.key; V pv = p.val;
      if ((ph & n) == 0)
      	  // 符合这一条件的节点放在原长度部分对应下标i中,并且上一个节点都作为下一个节点的next
          ln = new Node<K,V>(ph, pk, pv, ln);
      else
          // 符合这一条件的节点放在扩容长度部分对应下标 n+i 中,并且上一个节点都作为下一个节点的next
          hn = new Node<K,V>(ph, pk, pv, hn);
  }
  // 设置扩容后的table中 i (原长度部分对应位置)和 i+n (扩容部分对应位置)
  setTabAt(nextTab, i, ln);
  setTabAt(nextTab, i + n, hn);
  // 设置该节点已经成功转移,其他线程在帮助扩容时就不需要再处理该位置节点
  setTabAt(tab, i, fwd);
  advance = true;
}

线程锁住的是table中下标 i 处链表的第一个节点元素,配合可见性访问方法setTabAt对新表对应位置进行设置。

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

3、对于get操作

直接通过可见性访问接口拿到table中指定位置的链表,遍历链表比较key值即可

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
发布了95 篇原创文章 · 获赞 5 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43878293/article/details/103516647