day08-ConcurrentHashMap原理-1.7

背景

  • HashMap线程不安全:在https://blog.csdn.net/ym15229994318ym/article/details/105436994中提到多线程环境下,使用HashMap进行put操作时存在丢失数据、死锁的情况,为了避免这种bug的隐患,官方建议使用ConcurrentHashMap代替HashMap。
    效率低下的HashTable容器
  • HashTable效率低:HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
  • ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashMap,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

分段锁

感谢作者:https://www.cnblogs.com/ITtangtang/p/3948786.html

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
在这里插入图片描述
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

Segment

在ConcurrentHashMap内部,有一个Segment数组,Segment是什么呢?下图一目了然。
可以理解为一个自带锁的小的HashMap,当往ConcurrentHashMap集合中put一个元素时,先计算应该放在Segment[]数组的那个位置上,再调用对应位置Segment对象的put方法时,会计算应该放在小的HashMap数组的位置,这个小的HashMap中是有一个锁的,但是他与其他的小HashMap无关,这样就形成了一种分段锁。
在这里插入图片描述
Segment继承了ReentrantLock,所以Segment是一个自带锁的类。
在这里插入图片描述
Segment类有一个HashEntry<K,V>[] table;属性就是我这里说的小HashMap

ConcurrentHashMap原理

先明白几个变量

static final int DEFAULT INITIAL_CAPACITY = 16;//初识容量---initialCapacity
static final float DEFAULT_ LOAD_FACTOR = 0.75f;//加载因子---loadFactor 
static final int DEFAULT_CONCURRENCY_LEVEL = 16;//并发级别---concurrentLevel

会根据初识容量和并发级别计算出Segment数组的大小

初始化参数

  • 在创建ConcurrentMap集合时,提供的构造方法都会调用如下这个构造方法去做初始化,想HashMap一样依然先会去判断传入的参数是否合法,否则抛出异常。
  • 还会对传入的并发级别参数concurrentLevel进行处理,因为必须要满足2的幂次方。赋值为ssize
  • 在构造ConcurrentMap集合时,就已经根据处理后的参数,对Segment数组中的小HashMap进行了进行了配置,作为后面的模板,在插入元素时就不用每次都去计算小HashMap的容量等各个参数了。
    这里可以看出int c = initialCapacity / ssize //ssize是处理后的并发级别,并且得到的这个c值并不直接作为小HashMap的容量,也是经过位运算处理,使其变为2的幂次方。最终赋值给cap变量。
  • 接着直接创建容量为capHashEnry[],并赋值给创建的Segment对象。
  • 处理后的ssize作为Segment[]的容量。
    在这里插入图片描述
    这里的ssize和cap都做了位运算处理,使其变为2的指数次幂。原因:(在和元素的hash值做位运算时可以很好的散列,既要达到最可能的平均分配hashMap的value的在table数组的各个index,又要用二进制计算实现存取效率,就要要求hashMap的容量必须为2的幂次方;)

添加元素

添加元素时,逻辑是先找Segment数组下标位置,再在该Segment对象中的table数组找小HashMap下标位置。所以最后会调用segment对象的put方法,return s.put(key,hash,value,false);
判断Segment对应的这个下标下是否有元素(segment对象),为null就构造一个。
在这里插入图片描述
会去判断Segment对应的这个下标下是否有Segment对象,没有就构造一 个,在ensureSegment()方法中进行构造。这时构造就不需要去重新计算这个Segment对象的小HashMap的参数了,因为在创建ConcurrentMap对象时就已经初始化好了,放在Segment数组的第一个位置作为模板,使用该模板的参数即可。
在这里插入图片描述
这时Segment类的put方法。
在这里插入图片描述
这样就形成了分段锁,解决了HashMap线程不安全问题,也相对解决了HashTable效率慢的问题。
jdk7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在jdk8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.

源码执行图

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章