大厂面试真题-ConcurrentHashMap怎么保证的线程安全?

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它通过一系列精妙的机制来保证线程安全。以下是ConcurrentHashMap保证线程安全的主要方式:

  1. 分段锁(Segment Locking,Java 1.8之前)

    • 在Java 1.8之前的版本中,ConcurrentHashMap通过分段锁机制将整个哈希表分成多个段(Segment),每个段维护着一个独立的哈希表。
    • 读操作时,并不需要获取整个哈希表的锁,而是只需要获取对应段的锁。这样可以降低并发度限制,允许多个线程同时读取不同的段,从而提高读操作的并发性能。
    • 写操作时,需要锁定对应的段。虽然这仍然是一种锁机制,但相比于Hashtable等旧版哈希表的全局锁,分段锁能够显著提高并发性能。
  2. CAS(Compare and Swap)操作和synchronized机制(Java 1.8及以后)

    • Java 1.8版本的ConcurrentHashMap引入了新的数据结构和算法,进一步提高了并发性能。
    • 在写操作中,ConcurrentHashMap使用CAS操作来保证更新的原子性。CAS操作是一种无锁的同步机制,它通过比较内存中的值与期望值,如果相等则进行更新,否则重试。这种机制避免了全局锁的开销,提高了写操作的并发性能。
    • 同时,Java 1.8版本的ConcurrentHashMap也使用了synchronized机制来辅助实现线程安全,特别是在处理链表或红黑树等复杂数据结构时。
  3. 内部数据结构

    • ConcurrentHashMap使用了一种特殊的哈希表结构,即数组+链表+红黑树的结合体。
    • 当链表长度超过一定阈值时,会将链表转换为红黑树,以提高查找、插入和删除操作的效率。这种数据结构的设计使得在单个段上的操作更加高效。
  4. 读写分离

    • ConcurrentHashMap允许多个线程同时进行读取操作,而写入操作会引起更细粒度的锁定。这样,多个读操作可以并发进行,提高了并发性能。
    • 只有在写入操作时,才会对相关的段进行锁定,保证写入的原子性和一致性。
  5. 迭代器的线程安全性

    • ConcurrentHashMap的迭代器也是线程安全的。在迭代过程中,即使有其他线程对ConcurrentHashMap进行并发的修改操作,迭代器也不会抛出ConcurrentModificationException异常。
    • 这是因为迭代器在迭代期间保持对ConcurrentHashMap的结构的快照,而不是直接操作ConcurrentHashMap。

综上所述,ConcurrentHashMap通过分段锁(Java 1.8之前)、CAS操作和synchronized机制(Java 1.8及以后)、特殊的内部数据结构、读写分离以及线程安全的迭代器等多种机制来保证线程安全。这些机制共同作用下,使得ConcurrentHashMap能够在多线程环境中提供高效的并发性能和数据一致性。

以下是一个使用ConcurrentHashMap来保证线程安全的代码示例。这个例子将展示如何在多线程环境下安全地更新ConcurrentHashMap中的数据。

假设我们需要统计一个字符串中各个单词出现的次数,并且这个统计过程需要在多线程环境中进行。我们可以使用ConcurrentHashMap来存储单词及其出现的次数,并使用AtomicLong来保证计数操作的原子性(尽管在Java 8及更高版本中,ConcurrentHashMapcompute方法已经足够高效和线程安全,但这里也提供一个使用AtomicLong的示例作为对比)。

使用ConcurrentHashMapAtomicLong

 
import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.atomic.AtomicLong; 


public class WordCountExample { 
private final ConcurrentHashMap<String, AtomicLong> wordCounts = new ConcurrentHashMap<>(); 


public long increase(String word) { 
AtomicLong count = wordCounts.computeIfAbsent(word, k -> new AtomicLong(0)); 
return count.incrementAndGet(); 
} 


public static void main(String[] args) throws InterruptedException { 
WordCountExample example = new WordCountExample(); 


// 创建并启动多个线程来模拟并发增加单词计数 
Runnable task = () -> { 
for (int i = 0; i < 1000; i++) { 
String word = "example"; // 这里以固定单词为例,实际应用中可以是不同的单词 
example.increase(word); 
} 
}; 


Thread t1 = new Thread(task); 
Thread t2 = new Thread(task); 


t1.start(); 
t2.start(); 


t1.join(); 
t2.join(); 


// 打印最终结果 
System.out.println("Total count for 'example': " + example.wordCounts.get("example").get()); 
} 
}

 

使用ConcurrentHashMapcompute方法

如果不使用AtomicLong,而是直接使用ConcurrentHashMapcompute方法,也可以达到同样的效果,且代码更加简洁。

import java.util.concurrent.ConcurrentHashMap; 


public class WordCountExampleWithCompute { 
private final ConcurrentHashMap<String, Long> wordCounts = new ConcurrentHashMap<>(); 


public long increase(String word) { 
return wordCounts.compute(word, (k, v) -> v == null ? 1L : v + 1); 
} 


public static void main(String[] args) throws InterruptedException { 
// ...(与上面的main方法类似,只是实例化WordCountExampleWithCompute并调用其方法) 
} 
}

解释

  1. 使用AtomicLong
    • increase方法中,我们首先尝试从ConcurrentHashMap中获取与单词关联的AtomicLong对象。
    • 如果该单词不存在,则使用computeIfAbsent方法创建一个新的AtomicLong对象,并将其初始化为0。
    • 然后,使用incrementAndGet方法原子地增加计数器的值,并返回新的值。
  2. 使用compute方法
    • compute方法接受一个键和一个重映射函数,该函数根据键和当前值(如果存在)计算新值。
    • 如果当前值为null,则函数返回1(表示这是第一次添加该单词)。
    • 否则,函数返回当前值加1。

这两种方法都能在多线程环境中安全地更新ConcurrentHashMap中的数据,但使用compute方法的代码更加简洁。选择哪种方法取决于具体的应用场景和个人偏好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值