ConcurrentHashMap线程安全的实现原理

一、介绍

1.概念

ConcurrentHashMap是HashMap的线程安全版本,相对 HashMap 和 Hashtable, ConcurrentHashMap 增加了 Segment 层,每个 Segment 原理上等同于一个 Hashtable, ConcurrentHashMap 为 Segment 的数组。

向 ConcurrentHashMap 中插入数据或者读取数据,首先都要讲相应的 Key 映射到对应的 Segment,因此不用锁定整个类, 只要对单个的 Segment 操作进行上锁操作就可以了。理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。

注:hashmap的key和value可以为null,但是concurrentHashMap的key和value不能为null

2. ConcurrentHashMap 在 jdk1.7 的升级到 1.8 中的变化

  • 改进一:取消 segments 字段,直接采用 transient volatile HashEntry<K,V>[] table 保存数据,采用 table 数组元素作为锁,锁的粒度从多个 Node 级别又减小到一个 Node 级别,再度减小锁竞争,减小程序同步的部分。

  • 改进二:将原先 table 数组+单向链表的数据结构,变更为 table 数组+单向链表+红黑树的结构。


二、final关键字

在讲述ConcurrentHashMap类之前,先讲一下final关键字

(一)基础作用

  1. final修饰的字段不可修改,如果是引用,引用的地址不可改变。
  2. final修饰的方法不能重写
  3. final修饰的类不可继承

(二)final 的重排序规则

  • 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

简而言之:构造函数和对final域的读写操作不能重排序。

(三)final的使用场景

  • 任何不希望域被改变的时候都可以使用 final
  • 如果一个对象可以被多个线程访问到,要么被声明为 final,要么需要提供额外的线程安全机制。其他的方式包括声明为 volatile 、使用 synchronized、显示锁

三、实现

1.初始化操作(put操作)的线程安全

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;        
		//value 和 next 指针使用了 volatile 来保证其可见性
		...
}

有多个线程同时进行 put 操作(只有put操作的时候才会初始化数组),在初始化数组时使用了乐观锁 CAS 操作来决定到底是哪个线程有资格进行初始化,其他线程均只能等待。

  • volatile 变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由 volatile 保证。
  • CAS 操作:CAS 操作保证了设置 sizeCtl 标记位的原子性,保证了只有一个线程能设置成功

2.size()/统计当前存储元素个数的线程安全

  • 先利用 CAS 递增 Count 值来感知是否存在线程竞争,若竞争不大直接 CAS 递增 Count 值即可,性能与直接 Count++ 差别不大
  • 如果有线程竞争,则CAS失败,则初始化桶,利用桶计数,此时是分而治之的思想来计数,同时使用CAS来计数,最大化利用并行。如果桶计数失败,则扩容桶

在设计中,使用了分而治之的思想,将每一个计数都分散到各个 countCell 对象里面(下面称之为桶),使竞争最小化,又使用了 CAS 操作,就算有竞争,也可以对失败了的线程进行其他的处理。乐观锁的实现方式与悲观锁不同之处就在于乐观锁可以对竞争失败了的线程进行其他策略的处理,而悲观锁只能等待锁释放,所以这里使用 CAS 操作对竞争失败的线程做了其他处理,很巧妙的运用了 CAS 乐观锁。

发布了147 篇原创文章 · 获赞 835 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/qq_33945246/article/details/103956935
今日推荐