java并发之ConcurrentHashMap

Java并发】聊聊concurrentHashMap的put核心流程 阅读详情
 ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:

图表1

ConcurrentHashMap的主要优点在于提高了并发度,减小了锁的粒度。

每次只锁一个Segment

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

Segment

  我们再来具体了解一下Segment的数据结构:

1
2
3
4
5
6
7
static  final  class  Segment<K,V>  extends  ReentrantLock  implements  Serializable {
     transient  volatile  int  count;
     transient  int  modCount;
     transient  int  threshold;
     transient  volatile  HashEntry<K,V>[] table;
     final  float  loadFactor;
}

  详细解释一下Segment里面的成员变量的意义:

  • count:Segment中元素的数量
  • modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
  • threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
  • table:链表数组,数组中的每一个元素代表了一个链表的头部
  • loadFactor:负载因子,用于确定threshold

HashEntry

  Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构:

1
2
3
4
5
6
static  final  class  HashEntry<K,V> {
     final  K key;
     final  int  hash;
     volatile  V value;
     final  HashEntry<K,V> next;
}

  可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况。

ConcurrentHashMap的初始化

  下面我们来结合源代码来具体分析一下ConcurrentHashMap的实现,先看下初始化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public  ConcurrentHashMap( int  initialCapacity,
                          float  loadFactor,  int  concurrencyLevel) {
     if  (!(loadFactor >  0 ) || initialCapacity <  0  || concurrencyLevel <=  0 )
         throw  new  IllegalArgumentException();
  
     if  (concurrencyLevel > MAX_SEGMENTS)
         concurrencyLevel = MAX_SEGMENTS;
  
     // Find power-of-two sizes best matching arguments
     int  sshift =  0 ;
     int  ssize =  1 ;
     while  (ssize < concurrencyLevel) {
         ++sshift;
         ssize <<=  1 ;
     }
     segmentShift =  32  - sshift;
     segmentMask = ssize -  1 ;
     this .segments = Segment.newArray(ssize);
  
     if  (initialCapacity > MAXIMUM_CAPACITY)
         initialCapacity = MAXIMUM_CAPACITY;
     int  c = initialCapacity / ssize;
     if  (c * ssize < initialCapacity)
         ++c;
     int  cap =  1 ;
     while  (cap < c)
         cap <<=  1 ;
  
     for  ( int  i =  0 ; i <  this .segments.length; ++i)
         this .segments[i] =  new  Segment<K,V>(cap, loadFactor);
}

   CurrentHashMap的初始化一共有三个参数,一个initialCapacity,表示初始的容量,一个loadFactor,表示负载参数,最后一个是concurrentLevel,代表ConcurrentHashMap内部的Segment的数量,ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。

  整个ConcurrentHashMap的初始化方法还是非常简单的,先是根据concurrentLevel来new出Segment,这里Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash,加快hash的过程。接下来就是根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数,同样使为了加快hash的过程。

  这边需要特别注意一下两个变量,分别是segmentShift和segmentMask,这两个变量在后面将会起到很大的作用,假设构造函数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减一。

ConcurrentHashMap的get操作

  前面提到过ConcurrentHashMap的get操作是不用加锁的,我们这里看一下其实现:

1
2
3
4
public  V get(Object key) {
     int  hash = hash(key.hashCode());
     return  segmentFor(hash).get(key, hash);
}

  看第三行,segmentFor这个函数用于确定操作应该在哪一个segment中进行,几乎对ConcurrentHashMap的所有操作都需要用到这个函数,我们看下这个函数的实现:

1
2
3
final  Segment<K,V> segmentFor( int  hash) {
     return  segments[(hash >>> segmentShift) & segmentMask];
}

   这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。

  在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
V get(Object key,  int  hash) {
     if  (count !=  0 ) {  // read-volatile
         HashEntry<K,V> e = getFirst(hash);
         while  (e !=  null ) {
             if  (e.hash == hash && key.equals(e.key)) {
                 V v = e.value;
                 if  (v !=  null )
                     return  v;
                 return  readValueUnderLock(e);  // recheck
             }
             e = e.next;
         }
     }
     return  null ;
}

   先看第二行代码,这里对count进行了一次判断,其中count表示Segment中元素的数量,我们可以来看一下count的定义:

1
transient  volatile  int  count;

   可以看到count是volatile的,实际上这里里面利用了volatile的语义:

   写道

  对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。

  因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

  然后,在第三行,调用了getFirst()来取得链表的头部:

1
2
3
4
HashEntry<K,V> getFirst( int  hash) {
     HashEntry<K,V>[] tab = table;
     return  tab[hash & (tab.length -  1 )];
}

   同样,这里也是用位操作来确定链表的头部,hash值和HashTable的长度减一做与操作,最后的结果就是hash值的低n位,其中n是HashTable的长度以2为底的结果。

  在确定了链表的头部以后,就可以对整个链表进行遍历,看第4行,取出key对应的value的值,如果拿出的value的值是null,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value。

ConcurrentHashMap的put操作

  看完了get操作,再看下put操作,put操作的前面也是确定Segment的过程,这里不再赘述,直接看关键的segment的put方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
V put(K key,  int  hash, V value,  boolean  onlyIfAbsent) {
     lock();
     try  {
         int  c = count;
         if  (c++ > threshold)  // ensure capacity
             rehash();
         HashEntry<K,V>[] tab = table;
         int  index = hash & (tab.length -  1 );
         HashEntry<K,V> first = tab[index];
         HashEntry<K,V> e = first;
         while  (e !=  null  && (e.hash != hash || !key.equals(e.key)))
             e = e.next;
  
         V oldValue;
         if  (e !=  null ) {
             oldValue = e.value;
             if  (!onlyIfAbsent)
                 e.value = value;
         }
         else  {
             oldValue =  null ;
             ++modCount;
             tab[index] =  new  HashEntry<K,V>(key, hash, first, value);
             count = c;  // write-volatile
         }
         return  oldValue;
     finally  {
         unlock();
     }
}

   首先对Segment的put操作是加锁完成的,然后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash,关于rehash的过程大家可以自己去了解,这里不详细讲了。

  第8和第9行的操作就是getFirst的过程,确定链表头部的位置。

  第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素,如果找到,就直接更新更新key的value,如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。

ConcurrentHashMap的remove操作

  Remove操作的前面一部分和前面的get和put操作一样,都是定位Segment的过程,然后再调用Segment的remove方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
V remove(Object key,  int  hash, Object value) {
     lock();
     try  {
         int  c = count -  1 ;
         HashEntry<K,V>[] tab = table;
         int  index = hash & (tab.length -  1 );
         HashEntry<K,V> first = tab[index];
         HashEntry<K,V> e = first;
         while  (e !=  null  && (e.hash != hash || !key.equals(e.key)))
             e = e.next;
  
         V oldValue =  null ;
         if  (e !=  null ) {
             V v = e.value;
             if  (value ==  null  || value.equals(v)) {
                 oldValue = v;
                 // All entries following removed node can stay
                 // in list, but all preceding ones need to be
                 // cloned.
                 ++modCount;
                 HashEntry<K,V> newFirst = e.next;
                 for  (HashEntry<K,V> p = first; p != e; p = p.next)
                     newFirst =  new  HashEntry<K,V>(p.key, p.hash,
                                                   newFirst, p.value);
                 tab[index] = newFirst;
                 count = c;  // write-volatile
             }
         }
         return  oldValue;
     finally  {
         unlock();
     }
}

   首先remove操作也是确定需要删除的元素的位置,不过这里删除元素的方法不是简单地把待删除元素的前面的一个元素的next指向后面一个就完事了,我们之前已经说过HashEntry中的next是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去,看一下下面这一幅图来了解这个过程:
1
  假设链表中原来的元素如上图所示,现在要删除元素3,那么删除元素3以后的链表就如下图所示:
2


ConcurrentHashMap源码解析(二)之------put()方法。 当sizeCtl < 0 ,大概率为-1,其他线程正在进行创建table的过程,当前线程没有竞争到初始化table的锁。p是红黑树,如果与你插入节点的key有冲突点的话,则putTreeVal()方法会返回冲突节点的引用。再次判断table为null,防止其他线程已经初始化完毕,然后当前线程再次初始化,导致数据丢失。强制设置binCount为2,因为binCount 阅读详情

相关推荐

ConcurrentHashMap:使用方法和底层原理详解

ConcurrentHashMapJava集合框架中的一个线程安全的哈希表实现,它提供了比Hashtable和同步的HashMap更高的并发性能。ConcurrentHashMap在JDK 1.5中引入,通过采用分段锁的方式实现了高效的并发访问。它可以在保证线程安全的同时,提供较高的并发性能,因此是多线程环境下使用频率较高的数据结构之一。

jakelihua 4508

ConcurrentHashMap

在JDK1.4之前只有vector和HashTable是线程安全集合,在JDK1.5之后开始增加了安全的Map接口ConcurrentMap和线程安全的队列BlockIngQueue 通过继承关系可知: ConcurrentHashMap是HashMap的安全版本 ConcurrentMap 也是键值对形式存储数据 public interface ConcurrentMap<K, V> extends Map<K, V> { 实现自Map接口,即Map中所有的

iiiiiiiiiooooo的博客 979

Java并发ConcurrentHashMap详解

一、ConcurrentHashMap是什么?      ConcurrentHashMap是线程安全的并发容器,是用来替代在多线程环境下的HashMap,因为HashMap是线程不安全的,多线程环境下put操作可能会导致死循环,CPU利用率上升到100%。虽然有同步容器Hashtable和Collections.synchronizedMap方法使得容器同步,但是这些容器效率都非常低,因为它们锁...

LiQiyao的博客 2892

ConcurrentHashMap的put方法

ConcurrentHashMap的put方法解析ConcurrentHashMap的put方法JDK1.7put实现JDK1.8put实现直接插入新创建表插入插入过程扩容 ConcurrentHashMap的put方法 本文主要对ConcurrentHashMap的原理进行解析,主要是对比JDK1.7和JDK1.8的不同实现方式。 JDK1.7put实现 JDK1.7中引入SegmentSegment类通过继承ReentrantLock类,进行加锁,从而控制整个插入过程。 Segment数组也是一种数组

weixin_40156910的博客 8992

JavaConcurrentHashMap并发

ConcurrentHashMap 中的“并发度”(Concurrency Level)是指能够同时被修改的线程数量。并发度的主要目的是减少锁的争用,从而提高并发性能。在早期的 Java 版本中,ConcurrentHashMap 的内部实现是通过一个“分段锁”机制(Segment Locking)来实现的。这个机制将整个哈希表分为多个“Segment”,每个 Segment 都可以独立地进行加锁操作,因此多个线程可以同时对不同的 Segment 进行操作,而不会产生锁的竞争。

Flying_Fish_roe的博客 1193

Java并发编程利器:ConcurrentHashMap 详解

也提供了线程安全,但它使用的是全局锁,即所有方法都必须获取同一把锁才能执行。这种粗粒度的锁机制导致并发性能较低,在高并发场景下会成为性能瓶颈。它既保证了线程安全,又提供了优秀的并发性能,是 Java 并发编程中不可或缺的利器。它既保证了线程安全,又提供了优秀的并发性能,是高并发场景下的理想选择。的实现原理和使用技巧,你可以更好地掌握并发编程,构建高性能、高可靠性的应用程序。,使其线程安全,但这种方式的并发性能较低,因为所有操作都需要获取同一把锁。,从底层原理到使用技巧,助你掌握高并发场景下的数据存储。

q68686的博客 1074

Java ConcurrentHashMap并发安全实现原理解析

首先需要明确的是,C13Map的读操作一般是不加锁的(TreeBin的读解锁除外),而读操作与写操作有可能并行;可以保证的是,因为C13Map的写操作都要获取bin头部的syncronized互斥锁,能保证最多只有一个线程在做更新,这其实是一个单线程写、多线程读的并发安全性的问题。C13Map的get方法int n, eh;K ek;//执行扰动函数= null) {= null?= null) {

2401_84253850的博客 1204

java- ConcurrentHashMap 并发

在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率 a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。

wchg21131的博客 573

Java ConcurrentHashMap并发统计功能实现

在多线程编程中,我们经常需要对数据进行统计操作。普通的 HashMap 不是线程安全的,在多线程环境下使用会出现数据不一致的问题。而 ConcurrentHashMapJava 提供的线程安全的哈希表,它可以在多线程环境下高效地进行并发操作。本文的目的就是详细介绍如何利用 ConcurrentHashMap 实现并发统计功能,范围涵盖其核心概念、原理、代码实现以及实际应用等方面。

AI开发架构师 956

Java并发编程——ConcurrentHashMap详解

引出 场景:针对用户来做一个访问次数的记录。 通过HashMap进行记录,key为用户名,value为访问次数。 public class ConcurrentHashMapDemo { private static final HashMap<String, Integer> USER_ACCESS_COUNT = new HashMap<>(); public static void main(String[] args) { //针对用户来做

Java是世界上最好的语言 1万+

java并发-ConcurrentHashMapJava7 和 8 的区别

正是由于这些规则和要求的限制,红黑树保证了较高的查找效率,好处就是避免在极端的情况下冲突链表变得很长,在查询的时 候,效率会非常慢。,是一种平衡二叉查找树,查找效率高,会自动平衡,防止极端不平衡从而影响查找效率的情况发生。反观链表,它的时间复杂度就不一样了,如果发生了最坏的情况,可能需要遍历整个链表才能找到目标元素,时间复杂度为。put 方法的核心是 putVal 方法,为了方便阅读,我把重要步骤的解读用注释的形式补充在下面的源码中。,远远大于红黑树的 O(log(n)),尤其是在节点越来越多的情况下,

大乐乐 1411

JAVA并发集合之ConcurrentHashMap

ConcurrentHashMap

胡兵(Java) 1000

Java并发ConcurrentHashMap原理

HashTale与ConcurrentHashMapHashtable与ConcurrentHashMap都是线程安全的集合Hashtable并发度低,整个Hashtable对应一把锁,同一时刻,只能有一个线程操作它1.8之前ConcurrentHashMap使用了Segment+数组+链表的结构,每个Segment对应一把锁,如果多个线程访问不同的Segment,则不会冲突1.8之后ConcurrentHashMap将数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。

m0_53975926的博客 351

JavaConcurrentHashMap并发度是怎么样的

Java 7 和 Java 8 中的 ConcurrentHashMap并发度的实现上有所不同。Java 7 主要依赖分段锁技术来实现高并发性,而 Java 8 则采用了基于节点的细粒度锁策略,提供了更高的并发度和更好的性能。

小小鱼儿小小林的博客 314

Java并发编程之ConcurrentHashMap

在确定了链表的头部以后,就可以对整个链表进行遍历,看第4行,取出key对应的value的值,如果拿出的value的值是null,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value。因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

weixin_71690375的博客 927

Java并发编程:ConcurrentHashMap深度解析

在当今的互联网时代,高并发已经成为系统性能的关键考量因素。以Java为例,在高并发环境下,如何有效地管理并发访问和更新数据结构,成为了开发者必须面对的挑战。ConcurrentHashMap作为Java并发编程中的重要工具,其重要性不言而喻。想象一下,在一个大型在线交易系统中,成千上万的用户同时进行交易操作,如何确保数据的一致性和系统的稳定性?这正是ConcurrentHashMap能够发挥作用的场景。

JavaCodePro的博客 749

Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门

ConcurrentHashMapJava 中线程安全的哈希表实现,支持高并发读写操作。与 Hashtable 相比,它通过分段锁(JDK1.7)或 CAS + synchronized(JDK1.8)实现更细粒度的锁控制,兼顾性能与线程安全。下图为JDK1.7和1.8的ConcurrentHashMap:JDK1.8:让我们也对比下JDK1.7和JDK1.8的区别吧!对比维度JDK 1.7JDK 1.8数据结构Segment 数组(分段锁) + HashEntry 数组 + 链表。

flzjkl的博客 1160
上一篇: java Unsafe类的compareAndSwap方法
下一篇: Java并发编程:并发容器之CopyOnWriteArrayList(转载)
Golphing
博客等级 码龄12年 34粉丝 51原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值