`

concurrentHashMap同步实现原理

 
阅读更多
ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和HashTable功能相同但是线程安全的方法。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

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

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



Java代码  收藏代码
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的结构:



Java代码  收藏代码
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的实现,先看下初始化方法:



Java代码  收藏代码
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操作是不用加锁的,我们这里看一下其实现:



Java代码  收藏代码
public V get(Object key) { 
    int hash = hash(key.hashCode()); 
    return segmentFor(hash).get(key, hash); 


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



Java代码  收藏代码
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方法:



Java代码  收藏代码
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的定义:



Java代码  收藏代码
transient volatile int count; 

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



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

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



Java代码  收藏代码
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方法:



Java代码  收藏代码
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方法:



Java代码  收藏代码
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的size操作
在前面的章节中,我们涉及到的操作都是在单个Segment中进行的,但是ConcurrentHashMap有一些操作是在多个Segment中进行,比如size操作,ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。

前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了,具体的实现大家可以看ConcurrentHashMap的源码,这里就不贴了。
分享到:
评论

相关推荐

    ConcurrentHashMap之实现细节

    ### ConcurrentHashMap实现细节详解 #### 一、概述 `ConcurrentHashMap`是Java 5引入的一种高性能、线程安全的散列表实现。相较于传统的`HashMap`,`ConcurrentHashMap`能够支持高并发环境下的多线程读写操作。...

    Java中的ConcurrentHashMap:线程安全的哈希表实现与代码示例

    本文将深入探讨 ConcurrentHashMap 的内部实现原理,并通过代码示例展示其使用方法和优势。 通过本文,我们深入探讨了 ConcurrentHashMap 的内部实现原理,包括分段锁、CAS操作和红黑树等技术。此外,我们还通过代码...

    Java并发编程笔记之ConcurrentHashMap原理探究.docx

    在Java 7之前,ConcurrentHashMap主要依赖于Segment上的ReentrantLock来实现同步。每个Segment都有一个内部锁,当进行写操作时,会锁定对应Segment,读操作则不需要锁定。而从Java 8开始,ConcurrentHashMap改用CAS...

    java7-8中的 HashMap和ConcurrentHashMap全解析

    本文将深入解析这两个类在Java 7和8版本中的实现原理、特点以及使用场景。 首先,`HashMap`是Java中最基本的非线程安全的散列映射容器。它基于哈希表实现,提供O(1)的平均时间复杂度进行插入、删除和查找操作。在...

    Java Core Sprout:基础、并发、算法

    Java Core Sprout:一个萌芽阶段的Java核心知识库。...ConcurrentHashMap 的实现原理 如何优雅地使用和理解线程池 深入理解线程通信 一个线程召集的诡异事件 线程池中你不可错过的一些细节 『ARM包入坑指北』之队列

    java源码剖析-ConcurrentHashMap

    相比于传统的同步容器如`Hashtable`,`ConcurrentHashMap`提供了更精细的锁粒度,减少了锁竞争,从而提高了并发性能。 综上所述,`ConcurrentHashMap`作为Java并发编程中的一个核心组件,通过对`Segment`和`...

    Java 中ConcurrentHashMap的实现

    Java中的`ConcurrentHashMap`是Java 1.5版本引入的一种高效、线程安全的Map实现,它是`Hashtable`和`synchronized Map`的有力替代品。`ConcurrentHashMap`不仅保证了线程安全性,而且在多线程环境下的性能表现优于...

    ConcurrentHashMap共18页.pdf.zip

    这个压缩包包含了一份18页的PDF文档,很可能详细阐述了`ConcurrentHashMap`的设计原理、实现机制以及在实际应用中的使用方法。`ConcurrentHashMap`是Java集合框架中的并发容器,它在多线程环境下提供了高效且安全的...

    71-ConcurrentHashMap笔记1

    相较于早期版本,JDK1.8的ConcurrentHashMap放弃了Segment分段锁的设计,转而采用更细粒度的锁策略,结合Unsafe类的CAS操作和Node节点加锁,实现了高效且线程安全的并发哈希映射。 首先,让我们回顾一下HashMap和它...

    免费开源!!Java Core Sprout:基础、并发、算法

    常用集合 数组列表/向量 链表 哈希映射 ...ConcurrentHashMap 的实现原理 如何优雅地使用和理解线程池 深入理解线程通信 一个线程召集的诡异事件 线程池中你不可错过的一些细节 『ARM包入坑指北』之队列

    HashMap的实现原理

    下面我们将详细探讨HashMap的实现原理,包括其内部结构、插入操作流程以及扩容机制。 1. **内部结构** 在JDK 1.8之前,HashMap的底层结构是由`Entry`数组和链表组成的。每个`Entry`代表一个键值对,当多个键映射到...

    LRUCache实现 同步LRUCache

    下面我们将详细探讨LRUCache的原理和如何使用`LinkedHashMap`来实现。 首先,理解LRU策略的关键在于数据的访问顺序。每次访问的数据会被移到链表的尾部,如果新的访问请求导致缓存溢出,那么链表头部的数据(即最久...

    java软件技术文档-深入java8的集合5:Hashtable的实现原理.pdf

    尽管现在更多地使用HashMap或ConcurrentHashMap,但是了解Hashtable的实现原理仍然有助于深入理解Java的并发控制和数据结构。 **一、Hashtable概述** Hashtable类源自Java 1.0,它是一个基于哈希表的Map接口实现。...

    HashMap底层实现原理HashMap与HashTable区别HashMap与HashSet区别.docx

    HashMap的核心原理是通过散列函数将键对象转换为哈希码,然后使用这个哈希码来确定键值对在内部数组中的位置。哈希函数的设计使得相同的键会产生相同的哈希码,从而保证键值对的存储位置一致。然而,由于哈希表的...

    2019金三银四30家公司面试题汇总.doc

    Java 基础知识点包括集合框架的继承结构、异常的继承结构、HashMap 的实现原理和 ConcurrentHashMap 的实现原理等。集合框架是 Java 中的一个基本组件,提供了多种数据结构的实现,如 ArrayList、LinkedList、...

    基础知识.pdf

    重点讲解了HashMap的工作原理及代码实现,以及ConcurrentHashMap的实现原理,包括其线程安全的处理机制。 在核心篇中,深入讨论了数据存储相关的知识点,包括MySQL的JDBC流程,MVC设计思想,以及equals与==的区别。...

    集合底层原理总结

    本篇文章将总结集合框架的基础知识,包括主要接口、实现类以及底层实现原理。 1. 集合框架接口 - **Collection接口**:作为所有集合类的父接口,提供了基本的操作方法,如add、remove等。 - **List接口**:继承自...

    Java并发编程原理与实战

    JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 AbstractQueuedSynchronizer(AQS)详解.mp4 使用AQS重写自己的锁.mp4 重入锁原理与演示.mp4 读写锁认识与原理.mp4 细读...

    java 同步方法

    同时,理解并发编程的原理,如活锁、死锁、饥饿等问题,也是优化同步代码的关键。 总之,理解和优化Java同步方法的使用,是提高多线程应用程序性能和可伸缩性的核心。通过精细调整同步策略,可以有效地减少争用,...

Global site tag (gtag.js) - Google Analytics