Java并发编程系列-(5) Java并发容器 (8)

前面已经提到,JDK8和7在Resize的不同之处就是8保留了链表中元素的先后位置,这样基本可以确保在resize过程中不出现循环的问题,但是还是可能出现数据丢失的问题。以下是resize的核心实现,

Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } }

在实现中会使用两个临时链表,分别存储新地址和旧地址的链表,最后将这两个链表放到对应的位置。

假定出现如下的情况,有ABC三个元素需要移动,首先线程1指向A,next即为B,此后线程2同样进行resize,并把high/low两个链表的更新完成,这时返回线程1继续运行。

Image 3.jpeg

但是线程1仍然按照正常的流程继续,A会被放到High链表,B会被放到Low链表,这之后由于B后面没有元素,更新完成,因此C就漏掉了。

其实不管是JDK7还是8,由于链表的很多操作都没有加锁,每个操作也不是原子操作,导致可能出现很多意想不到的结果,也是为什么需要引入专门的ConcurrentHashMap。

ConcurrentHashMap介绍

为什么不使用HashTable?

之前介绍的HashTable也能保证线程安全,但是HashTable使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。正因为如此,需要引入更加高效的多线程解决方案。

ConcurrentHashMap的结构在JDk1.7和1.8中有较大的不同,下面将会分别进行介绍。

JDK1.7中的实现

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际继承自可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,每个Segment里包含一个HashEntry数组,我们称之为table,每个HashEntry是一个链表结构的元素。

Segment实际继承自可重入锁(ReentrantLock),这是与普通HashMap的最大区别。

Picture1.png

面试点:ConcurrentHashMap实现原理是怎么样的或者ConcurrentHashMap如何在保证高并发下线程安全的同时实现了性能提升?

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,只要多个修改操作发生在不同的段上,它们就可以并发进行。

1.1 初始化过程

初始化有三个参数:

initialCapacity:初始容量大小 ,默认16。

loadFactor, 扩容因子或者叫负载因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。

concurrencyLevel 并发度:默认16。并发度可以理解为程序运行时能够同时操作ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

以下是对初始化函数的分析:

Screen Shot 2019-12-09 at 10.47.06 PM.png

1.2 Hash值计算

对某个元素进行Put/Get操作之前,都需要定位该元素在哪个segment元素的某个table元素中的,定位的过程,取得key的hashcode值进行一次再散列(通过Wang/Jenkins算法),拿到再散列值后,以再散列值的高位进行取模得到当前元素在哪个segment上。

Screen Shot 2019-12-09 at 11.10.10 PM.png

具体的Hash实现如下:

Screen Shot 2019-12-09 at 11.12.01 PM.png

1.3 Get方法

定位segment和定位table后,依次扫描这个table元素下的的链表,要么找到元素,要么返回null。

在高并发下的情况下如何保证取得的元素是最新的?

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwzxpw.html