ConcurrentHashMap

本文探讨了如何实现HashMap的线程安全性,重点介绍了ConcurrentHashMap的结构和优化策略,包括分段锁的概念,以及Unsafe类在其中的作用。内容涵盖了ConcurrentHashMap的put方法、Segment对象的生成与同步策略,以及Unsafe类提供的CAS乐观锁机制在保证并发安全性中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.hashmap如果要保证同步,该如何实现

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200920101717911.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3ODAyNjM3,size_16,color_FFFFFF,t_70#pic_cente
我们知道,如果多个线程同时访问hashtable时候,hashtable是直接将此对象锁起来。加入hashmap也按照hashtable这种方式来实现,即put、remove等方法上加一个syncronzed关键字。这种情况效率肯定不高。如上图,如果把hashmap分成一段一段的,加入一个12:1这个值,你会发现受影响的只是倒数第二段,其他的段里的操作不会受印象。所以concurrentHashMap加入了段的概念来优化性能。

2.concurrentHashMap结构

在这里插入图片描述
此构造方法第一个参数表示默认初始化容量16,表示ConcurrentHashMap的初始化容量大小,在hashmap里由于
在初始化数组时候会调用,table = new Entry[capacity];(capacity已经被设置为了2的幂)此方法,所以数组大小默认也为16,
但是在concurrenthashmap中

,第二个表示默认初始化负载因子0.75,第三个参数表示默认并发级别,也就是concurrenthashmap有多少段,默认16段。

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;                                  // 1
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;                         // 2
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;  //11
    while (cap < c)  //22
        cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);  //33
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];   //3
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

查看1、2、3可以发现,segment的值是根据concurrencyLevel来设定的,ssize值为大于等于concurrencyLevel,并且是2的幂
查看11、22、33可以发现,cap值初始为2(MIN_SEGMENT_TABLE_CAPACITY值为2),查看22可以发现,cap的值和c有关。
而c的值是concurrentHashmap的初始化大小/段数组大小,如果有余数,则将c值再加1.例如假设初始化容量为33.段数组大小为16,33/16=2…1,那么c的值就为3.结合实际也可以容易分析出来。再回到22中,cap的值最终被设定为大于等于c的2的幂。所以cap的值为4。所以假设初始化容量为33.段数组大小为16时,每段中的数组大小为4.
在这里插入图片描述
看ConcurrentHashMap可以发现,它是数组的初始大小只设置了一个,如图所示为2,其他segment下的数组大小根据这个s0即第一个段大小设置为相同的值2.如果某一段下的数组满了并且来了一个新元素那么只需要将这段下的数组进行扩容就好了

unsafe方法

Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。
Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。

Unsafe类是"final"的,不允许继承。且构造函数是private的:

在上图倒数第三行中可以看到unsafe方法,
在这里插入图片描述
如上图可以看到它的get方法,首先会得到当前调用类的类加载器,如果是自己编写的java类,那么它的类加载器就是Application,应用程序类加载器;如果是外部java类,那么就是bootStrap,系统类加载器,在应用程序类它得到的是null,所以getUnsafe方法会区分当前调用的类是jdk中的类还是程序员自己编写的类,如果是程序员自己编写的,那么在获取unsafe对象的时候会抛出异常。因为unsafe方法的调用很容易导致一些问题,所以jdk不建议程序员调用unsafe方法,但是这个东东在jdk中用到的太多了,不说下不痛快!

那么如果想得到unsafe对象,怎么办?如下所示,通过反射来获取对象。
在这里插入图片描述
为什么用unsafe
unsafe详解

我真的写吐了,每次写到一半,保存了第二天就没有了,我靠靠靠!!!!!!!!!!!!!!!!!!!!!!
真的是厕所里跳远------过分

ConcurrentHashMap的put方法

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

put方法中Segement对象的生成及其位置

int j = (hash >>> segmentShift) & segmentMask;
其中segementShift的值在concurrentHashMap的构造方法可以看到
int sshift = 0;
int ssize = 1; // 1
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1; // 2
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
假设concurrencyLevel的大小为16,ssize大小为16时才会推出循环,此时sshift的大小为4,其实就是2^4=16中的4。segementShift的大小为32-4 = 28 。而segementMask的大小为15.
int j = (hash >>> segmentShift) & segmentMask;

      hash >>> segmentShift 表示将得到的hash值右移28位,其实就是取最高的4位(int类型的hash值占32位)
      例如:hash值为        1011  0100 0101 1111 1110  1100  0011 0101
      右移之后得到的就是     0000  0000 0000 0000 0000  0000  0000 1011
      和16与操作后          0000  0000 0000 0000 0000  0000  0000 1111
      得到的值就是          0000  0000 0000 0000 0000  0000  0000 1011
      即 j=11。

s = ensureSegment(j);如下

/**
 * Returns the segment for the given index, creating it and
 * recording in segment table (via CAS) if not already present.
 *
 * @param k the index
 * @return the segment
 */
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}
可以看注释,它是返回一个segement数组下标为j的Segement对象。

Segement对象的同步

在多线程情况下,如何保证多个线程只会创建一个segement并且使用同一个segment。

k其实就是数组下标,(k << SSHIFT) + SBASE获取的值也是k,这样做的目的是保证在多线程同步,如果直接使用segement[j]来获取数组的第j个元素的话,这样做和单线程没啥区别,毕竟你不知道别的线程有没有创建这个segement对象。加锁能解决问题,但是这里concurrentHashMap采用了UnSafe方法(SBASE中的) ,也就是CAS的乐观锁方式来处理。这个比较难以理解,反正以后看到这种就当做k值来。
对于 CAS 系列方法的具体使用方法就不在这里赘述了,我们都知道该方法的作用是原子操作比较并交换两个值,运用的时底层硬件所提供的 CAS 支持,在 Java API 中我们可以看到该方法具有四个参数。

obj :包含要修改的字段对象;
offset :字段在对象内的偏移量;
expect : 字段的期望值;
update :如果该字段的值等于字段的期望值,用于更新字段的新值;

compareAndSwapObject 方法其实比较的就是两个 Java Object 的地址,如果相等则将新的地址(Java Object)赋给该字段。
保证segement对象的同步主要是使用CAS,在这里不在深究。
详细介绍如下:
源码解析 Java 的 compareAndSwapObject 到底比较的是什么?

回到上面,就比较容易理解了,就算线程1先执行但是没有创建一个segement对象,线程2后执行创建了对象,由于CAS的存在,线程1是会使用线程2的对象的。

### ConcurrentHashMap 使用指南 ConcurrentHashMap 是 Java 提供的一种线程安全的 Map 实现,专为高并发环境设计。相比传统的 HashMap 和 Hashtable,它在多线程访问时表现出更高的性能和安全性。通过分段锁机制与 CAS(Compare and Swap)操作,ConcurrentHashMap 能够支持多个线程同时读写而无需全局加锁。 #### 线程安全特性 ConcurrentHashMap 的线程安全特性来源于其内部实现机制。不同于 Hashtable 使用的 synchronized 方法进行全局加锁,ConcurrentHashMap 在 JDK 1.7 及之前版本中采用 **分段锁(Segment Locking)** 技术[^3]。每个 Segment 相当于一个独立的 HashTable,允许多个线程同时访问不同的 Segment,从而减少锁竞争,提高并发性能。 在 JDK 1.8 中,ConcurrentHashMap 进一步优化了其实现,使用 **桶锁(Bucket-level Locking)** 和 **红黑树结构** 来提升性能。此时,锁的粒度更细,仅对当前操作的桶进行加锁,进一步提高了并发能力[^5]。 #### 常用方法与并发操作 ConcurrentHashMap 提供了一系列线程安全的操作方法,例如: - `putIfAbsent(K key, V value)`:只有当指定键不存在或映射值为 null 时才插入键值对。 - `computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)`:如果键存在,则根据提供的函数重新计算该键的值。 - `compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)`:无论键是否存在,都尝试重新计算其映射值。 - `forEach(BiConsumer<? super K, ? super V> action)`:对每个键值对执行指定操作。 这些方法可以在不使用外部同步的情况下安全地用于多线程环境中[^2]。 #### 示例代码 以下是一个简单的示例,展示如何在 Spring Boot 应用中使用 ConcurrentHashMap 管理共享数据: ```java import java.util.concurrent.ConcurrentHashMap; public class UserService { private final ConcurrentHashMap<String, Integer> userCountMap = new ConcurrentHashMap<>(); public void incrementUserCount(String department) { userCountMap.compute(department, (key, oldValue) -> (oldValue == null) ? 1 : oldValue + 1); } public int getUserCount(String department) { return userCountMap.getOrDefault(department, 0); } } ``` 上述代码中的 `compute` 方法确保在多线程环境下对用户计数的更新是线程安全的,无需额外的同步控制[^4]。 #### 适用场景 ConcurrentHashMap 非常适合以下场景: - **缓存系统**:如 Web 应用中缓存用户信息、会话状态等。 - **计数器**:统计在线人数、请求次数等。 - **任务调度**:管理待处理任务队列或资源池。 - **配置中心**:存储可动态更新的配置项。 由于其良好的并发性能,ConcurrentHashMap 成为了构建高性能服务端应用的重要组件之一。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值