ConcurrentHashMap简介

发布时间:2024-01-11 00:20:34 来源:君肯网

先贴出一张图表示对ConCurrentHashMap的理解

HashMap: HashEntry数组

HashEntry :注意 value 以及 next域都用volatile修饰,保证数据安全。

Segments :并发的最小单元,ConcurrentHashMap与Hashtable不同的是,ConcurrenHashMap是分段加锁,而Hashtable则是整个对象加锁。从加锁的方式开看,ConcurrentHashMap效率相对来说高一点。每个Segments都是一个小型的HashMap。

Segment 继承了可重入锁,提升并发操作的效率。

ConcurrentHashMap数据插入:

这只是读源码笔记,主要将自己的感受记下来,写的不好的地方请大家原谅。如果对您有帮助那是莫大的荣幸了,同时想说 纸上读来终觉浅 ,感兴趣的同学可以翻看一下源码,会有意想不到的收获。

hashmap和concurrenthashmap的区别是什么?

从源码来窥其一斑!

我们都知道hashMap不是线程安全的,因为在扩容方法中很容易出现死循环,hashTable使用锁的方式比较简单暴力,几乎在所有操作方法上都加了synchronized锁,导致总体性能很差,concurrentHashmap凭借线程安全且性能优异一直都是高并发中的首选key-value型数据结构;

concurrentHashmap的高性能有以下原因:

一,分段锁:jdk8中对concurrentHashmap进行了改进,抛弃了jdk7中新建segment作为分段锁的过程,jdk8中虽沿用了这种分段锁的思想,却直接使用数组中的数据作为 分段锁保证concurrentHashmap在上锁的时候只针对数组下标下的数据进行上锁 (比如如果数组长度为256,那么每次put平均只有1/256的数据被锁),而大多数其他的数据还是能进行正常的增删改操作,无需阻塞等待,这无疑极大的 降低了锁的粒度,提升了性能。

二,红黑树 :jdk8中引入了红黑树结构,在单个数组下标内的数据达到8以后,会自动转换为红黑树进行存储, 使用大O表示法表示效率的话,红黑树的查找效率为O(log(n)),而链表的效率为O(n) ,当数据量越来越大的时候,红黑树的效率明显好于链表,所以concurrentHashmap性能得到很大提升;

现在我们主要从put方法中的主要方法来分析性能的提升:

spread(key.hashCode())//作用是再次哈希,减少冲突 ,源码如下

其中涉及到的位运算有

&gt&gt&gt16:无符号右移16位,空位以0补齐 。

^:异或运算符–&gt相同为0,不同为1; &amp:与运算符–&gt全1得1,否则0;

(h ^ (h &gt&gt&gt16)) &ampHASH_BITS所以这句代码的意思就是不仅消除高16位的影响,同时获得正整数的hash值

再来看后面的方法, 如上图:

1,就是判断当这个hash表还是空的时候,调用initTable进行初始化; 2,使用(n – 1) &amphash)计算数组下标,如果数据指定下标处为null,则直接插入,注: cas是java8中的concurrentHashmap引入的线程安全判断,CAS算法做为乐观锁 ;

3,(fh = f.hash) == MOVED,走到此处说明下标内有node,且该node的值为-1(MODED=-1),搜索全类发现MODED是在调用有参构造器ForwardingNode中默认写入的,而这个调用处刚好在transfer方法中,所以我们推断,扩容的时候先将数组下标内的node.hash置为-1! 同时在3这一步中调用helpTransfer(tab, f)参与扩容,并把数据写入;

4,走到这说明node不是空的,也没在扩容,那么锁住该下标下的node,并把新value插入链表中; 5,如果锁住的这个node能实例化为TreeBin,则代表已经转化为红黑树进行存储,将数据插入红黑树中; 6,判断在4,5中计算得到的数组下标内所有节点总数, 如果满足转化为红黑树的条件(节点数大于8),则自动转化为红黑树进行存储!

总的来说,concurrentHashmap之所以性能高就是因为使用了分段锁和红黑树!

至于conrrentHashmap其他的方法的源码分析,后期会补上的,更多的技术分享,敬请关注!

ConcurrentHashMap原理和使用

hashmap和concurrenthashmap的区别如下:

HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在。

那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

HashMap:

底层数组+链表实现,可以存储null键和null值,线程不安全。

初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂。

扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。

插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)。

ConcurrentHashMap:

底层采用分段的数组+链表实现,线程安全。

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)。

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。

ConcurrentHashMap内部排序的使用

ConcurrentHashMap是线程安全的,使用环境大多在多线程环境下,在高并发情况下保证数据的可见性和一致性。

HashMap是一种键值对的数据存储容器,在JDK1.7中使用的是数组+链表的存储结构,在JDK1.8使用的是数组+链表+红黑树的存储结构,关于HashMap的实现原理可以查看 《hashMap实现原理》 。

HashMap是线程不安全的,主要体现在多线程环境下,容器扩容的时候会造成环形链表的情况,关于hashMap线程不安全原因可以查看 《HashMap线程不安全原因》

HashTable也是一种线程安全的键值对容器,我们一般都是听说过,具体使用较少,为什么线程安全的HashTable在多线程环境下使用较少,主要原因在于其效率较低(虽然效率低,但是很安全在多线程环境下)。

下面来分析一下HashTable为什么线程安全,但是效率较低的原因?

ConcurrentHashMap简介

查看HashTable类,我们发现在源码中所有的方法都加上了synchronized同步关键字,这也就保证的在多线程环境下线程安全,由于所有的方法都加上了synchronized,这也就导致在一个线程获取到HashTable对象锁的时候,其他线程是不能访问HashTable中的其他方法的,比如A线程在使用HashTable的get()方法时,当B线程想使用HashTable的put()方法的时候必须等到A线程使用完get()方法并释放锁,而且B线程正好能够获取到HashTable的锁的时候才行,这样在多线程环境下就会造成线程长时间的阻塞。使其效率底下的原因所在。

在HashTable中由于在方法上设置synchronized,导致虽然是线程安全的,但是只有一个HashTable的对象锁,也就是说同一时刻只能有一个线程可以访问HashTable,线程都必须竞争同一把锁。假如容器中里面有多把锁,每把锁用于锁住容器的一部分数据,那么当多线程访问容器里面不同的数据时,多线程之间就不会存在锁的竞争,从而提高了并发访问的效率,这就是ConcurrentHashMap的 锁分段技术 。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁

ConcurrentHashMap的初始化

在ConcurrentHashMap中如何将数据均匀的散列到每一个Segment中?如果数据不能均匀的散列到各个Segment中,那么ConcurrentHashMap的并行性就会下降,好比,所有的数据都在一个Segment中,那么一个ConcurrentHashMap中就相当于只有一个锁,这和HashTable没有什么区别,所以通过一个hash()方法,将数据均匀的散列到不同的Segment中去(注意,虽然经过散列数据会均匀分散的不同的Segment中去,但是,也会有可能出现一个Segment中数据过多的问题,如果数据过多,多线程访问到的概率就会增加,导致并行度下降,如何优化解决ConcurrentHashMap中锁的加入时机和位置,这就是JDK1.8对ConcurrentHashMap所要做的事情)

查看ConcurrentHashMap的put()方法

由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。

(1)是否需要扩容

在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

(2)如何扩容

在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile类型(关于volatile可以查看 《volatile synchronized final的内存语义》 ),如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。

锁的优化

在前面我们提到,在JDK1.7中ConcurrentHashMap使用锁分段的技术,提高了ConcurrentHashMap的并行度,虽然经过散列数据会均匀分散的不同的Segment中去,但是也会有可能出现一个Segment中数据过多的问题,如果数据过多,多线程访问到的概率就会增加,导致并行度下降。

在JDK1.8中ConcurrentHashMap细化了锁的粒度,缩小了公共资源的范围。采用synchronized+CAS的方式实现对共享资源的安全访问,只锁定当前链表或红黑二叉树的 首节点 ,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

结构优化

在JDK1.7中ConcurrentHashMap的结构是数组+链表,我们知道链表随机插入和删除较快,但是查询和修改则会很慢,在ConcurrentHashMap中如果存在一个数组下标下的链表过长,查找某个value的复杂度为O(n),

在JDK1.8中ConcurrentHashMap的结构是数组+链表+红黑树,在链表的长度不超过8时,使用链表,在链表长度超过8时,将链表转换为红黑树复杂度变成O(logN)。效率提高

下面是JDK1.8中ConcurrentHashMap的数据结构

TreeBin:红黑树数节点     Node:链表节点

在JDK1.8中,hash()方法得到了简化,提高了效率,但是增加了碰撞的概率,不过碰撞的概率虽然增加了,但是通过红黑树可以优化,总的来说还是相较JDK1.7有很大的优化。

下面来分析put(),来观察JDK1.8中如何采用synchronized+CAS的方式细化锁的粒度,只锁定当前链表或红黑二叉树的 首节点。

业务中,我们经常会有队map进行排序的要求,如下将会详细讲解如何利用java8的lambda表达式实现map的内部排序。

首先,我们先构造一个person类:

public class Person {

private String addr

private String age

}

测试,对map进行排序处理。

import com.ky.common.Person

import java.util.*

import java.util.concurrent.ConcurrentHashMap

import java.util.stream.Collectors

import java.util.stream.Stream

import static java.util.Map.Entry.comparingByValue

import static java.util.stream.Collectors.toMap

public class Test {

stream.sorted(Comparator.comparingInt(Map.Entry::getValue)).collect(Collectors.toMap(Map.Entry::getKey,

Map.Entry::getValue,(e1, e2)-&gte2, ConcurrentHashMap::new))

map.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getValue)).forEach(System.out::println)

}

以上就是关于ConcurrentHashMap简介全部的内容,如果了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

更多相关资讯

先贴出一张图表示对ConCurrentHashMap的理解 HashMap: HashEntry数组 HashEntry :注意 value…
查看详情
先贴出一张图表示对ConCurrentHashMap的理解 HashMap: HashEntry数组 HashEntry :注意 value…
查看详情
先贴出一张图表示对ConCurrentHashMap的理解 HashMap: HashEntry数组 HashEntry :注意 value…
查看详情
相关文章
推荐游戏
风之谷
风之谷
游戏资讯 10.5M
下载
斗罗大陆3
斗罗大陆3
游戏资讯 566.9M
下载
冠军网球
冠军网球
游戏资讯 148.1M
下载
最佳炮手
最佳炮手
游戏资讯 68.1M
下载
如梦下弦月
如梦下弦月
游戏资讯 840.1M
下载
富甲封神传
富甲封神传
游戏资讯 263.0M
下载