ConcurrentHashMap 加锁

`ConcurrentHashMap`(CHM)在Java的不同版本中采用了不同的加锁策略来保证线程安全性,同时尽可能地提高并发性能。下面分别介绍Java 1.7及之前的版本和Java 1.8及之后的版本中`ConcurrentHashMap`的加锁机制:

### Java 1.7及以前版本

在Java 1.7中,`ConcurrentHashMap`使用了**分段锁(Segment Locking)**的设计。它将整个Map划分为多个Segment(段),每个Segment继承自`ReentrantLock`,并且包含了一个散列桶数组。当进行写操作时,不是对整个Map加锁,而是仅仅对涉及操作的Segment加锁,这样就大大降低了锁的粒度,提高了并发性能

### Java 1.8及以后版本

从Java 1.8开始,`ConcurrentHashMap`进行了重大重构,移除了Segment结构,转而使用了**CAS(Compare and Swap)+ synchronized关键字**的组合方式来实现并发控制。主要改进点包括:

- **CAS操作**:大部分操作(如插入、删除、更新等)都是基于CAS操作来实现无锁化的更新,避免了线程之间的竞争。

- **Node节点**:存储元素的数据结构由链表+红黑树组成,每个节点(Node)都有自己的标志位,用于表示节点的状态以及原子地修改节点。

- **局部化锁定**:尽管去掉了Segment结构,但在某些特定条件下(如扩容、解决冲突等)仍然需要加锁,但此时的加锁范围进一步缩小到了桶(bucket)级别。当多个线程试图修改同一个桶时,才会发生锁竞争,而对不同桶的操作可以并发进行。

- **内部类`ForwardingNode`**:在扩容过程中,引入了转发节点的概念,使得扩容过程可以渐进式地进行,进一步减少了锁竞争。

总结起来,`ConcurrentHashMap`在Java 1.8之后通过以下几个方面降低锁竞争并提高并发能力:
- 使用细粒度的CAS操作代替传统的锁。
- 在必要时(如修改链表头结点或扩容时)使用`synchronized`关键字对桶进行锁定。
- 利用红黑树优化冲突较多时的查找和更新性能。
- 分布式锁的应用,即仅对涉及到的桶进行加锁,而不是整个数据结构。

“Map分割成多个Segment”特指的是Java 1.7及更早版本的`ConcurrentHashMap`实现中的分段锁机制。在这个版本中,`ConcurrentHashMap`通过Segment来达到细粒度的并发控制。

具体分割过程简述如下:

1. 初始化时,`ConcurrentHashMap`会根据预设的并发级别(默认为16)决定Segment的数量。这个并发级别对应的就是Segment数组的长度。

2. 当向`ConcurrentHashMap`中插入键值对时,首先通过散列函数计算键的哈希值,然后根据哈希值确定该键值对应存储在哪个Segment中。通常是对哈希值进行再散列运算(取模运算符 `%`),以保证分布均匀。

3. 这样,整个Map就被逻辑上分割成了多个Segment,每个Segment维护着一部分键值对,并且拥有各自的锁。当对某个键值对进行操作时,只需锁定对应的Segment,而不会影响到其他Segment,从而实现了并发访问和修改。

举个例子,假设并发级别为16,那么一个`ConcurrentHashMap`会被划分为16个Segment。当插入一个键值对时,通过哈希运算确定它应该存放在第i个Segment中,则只对第i个Segment加锁,其它Segment不受影响,可以并发地进行读写操作。

在Java 1.7版本的`ConcurrentHashMap`中,的确存在多次散列运算的过程,目的是为了将键映射到正确的Segment(段)以及在Segment内部找到正确的位置存放键值对。这里的散列运算主要包括两步:

1. **定位Segment**:首先,对键进行一次散列运算得到一个哈希码。然后,将这个哈希码与`ConcurrentHashMap`的并发级别进行取模运算(`hash % concurrencyLevel`),得到的余数就是Segment数组的索引,从而定位到对应的Segment。

2. **定位桶(Bucket)**:在确定了Segment之后,还需要在Segment内部找到确切的位置存储键值对。这一步也会进行散列运算,通常是再次对键的哈希码进行某种变换(如再次散列或者简单的按位运算),然后对Segment内部的桶数组长度取模,得出的结果就是键值对在桶数组中的位置索引

举例来说,如果并发级别为16,那么Segment数组的长度就是16。对于任意一个键,首先计算出它的哈希值,然后对该哈希值取模16,就可以确定键值对应存储在Segment数组的哪一个Segment中。接着,在确定的Segment内部,再次对键进行散列运算,并对Segment内部的桶数组长度取模,确定键值对在桶数组中的具体位置。

这种设计极大地减少了并发环境下对共享资源的竞争,提升了整体的并发性能。而在Java 1.8及以后的版本中,虽然取消了Segment结构,但类似的散列运算依然存在,只不过是为了定位到数组的具体槽位,而且利用了CAS操作和其他并发优化技术。

HashMap在存储键值对时的过程大致如下:

1. **计算哈希值**:
   当我们将一个键值对插入到HashMap时,首先会对键(Key)进行哈希运算,生成一个哈希码。哈希函数的目标是尽量分散输入键值的空间分布,减少冲突的可能性。

2. **索引定位**:
   根据生成的哈希码和HashMap的容量(数组长度),通过取模运算(`hashcode % capacity`)确定键值对在数组中的索引位置。这个位置称为桶(bucket)。

3. **解决冲突**:
   如果两个不同的键经过哈希运算后,指向了同一个桶,就会产生哈希冲突。HashMap通过链地址法来解决冲突,即将冲突的键值对链接在一个链表上,每个桶对应的位置存储的是链表的头结点。

   - **链表**:在Java 7及更早版本中,当发生冲突时,新的键值对将以链表的形式插入到相应桶的位置。
   - **红黑树**:从Java 8开始,当链表长度超过阈值(默认是8)时,HashMap会自动转换该链表为一颗红黑树,这样可以保持O(log n)的查询复杂度。

4. **插入操作**:
   插入操作会在找到桶的位置后,如果是空桶,则直接新建一个Entry(节点)放入;如果是链表,就在链表尾部追加新的Entry;如果是红黑树,则按照红黑树的插入规则插入新的节点。

5. **扩容**:
   当HashMap中的元素数量超过负载因子(load factor,默认是0.75)与当前容量的乘积时,将会触发扩容操作,容量翻倍,并将所有的键值对重新计算索引位置后放入新的数组中,这一过程也称为rehashing。

6. **细节处理**:
   在插入过程中,会考虑到线程安全问题。在Java 8以前的版本中,只有在单线程环境下才无需额外同步;而在Java 8及其后续版本中,即使在多线程环境下,HashMap也具备了一定程度的线程安全性,主要是通过CAS操作和同步代码块来保证。

综上所述,HashMap存储键值对是一个综合运用哈希算法、链表或红黑树数据结构以及可能的扩容机制的过程,旨在高效地查找、插入和删除元素。

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值