Java并发编程的艺术:(6)Java 并发容器和框架之 ConcurrentHashMap


Java 语言内置了非常多的并发容器和框架,相比其他语言进行并发编程时非常方便。

ConcurrentHashMap 的实现原理与使用

ConcurrentHashMap 是线程安全且高效的 HashMap。

为什么要使用 ConcurrentHashMap

在并发编程中使用 HashMap 可能导致程序死循环。而使用线程安全的 HashTable 效率又非常低下,基于以上两个原因,便有了 ConcurrentHashMap 的登场机会。

线程不安全的 HashMap

在多线程环境下,使用 HashMap 进行 put 操作会引起死循环,导致 CPU 利用率接近 100%,所以在并发情况下不能使用 HashMap。例如,执行以下代码会引起死循环。

final HashMap<String, String> map = new HashMap<String, String>(2);
		Thread t = new Thread(new Runnable() {
			
			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					new Thread(new Runnable() {
						
						@Override
						public void run() {
							map.put(UUID.randomUUID().toString(), "");
							
						}
					}, "ftf" + i).start();
				}
				
			}
		}, "ftf");
		t.start();
		t.join();

HashMap 在并发执行 put 操作时会引起死循环,是因为多线程会导致 HashMap 的 Entry 链表形成环形数据结构,一旦形成环形数据结构,Entry 的 next 节点永远不为空,就会产生死循环获取 Entry。

效率低下的 HashTable

HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下。

ConcurrentHashMap 的锁分段技术可有效提升并发访问率

HashTable 容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问 HashTable 的线程都必须竞争同一把锁,加入容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么多线程访问容器里不同数据段的数据时,线程间就不会存在竞争,从而可以有效提高并发访问效率,这就是 ConcurrentHashMap 所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中其中一个段数据的时候,其他段的数据也可能被其他线程访问。

ConcurrentHashMap 的结构

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构构成。Segment 是一种可重入锁,在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得与它对应的 Segment 锁。

ConcurrentHashMap 的操作

ConcurrentHashMap 常用的三个操作为:get 操作、put 操作和 size 操作。

get 操作

Segment 的 get 操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素,代码如下。

public v get(Object key) {
	int hash = hash(key.hashCode());
	return segmentFor(hash).get(key, hash);
}

get 操作的高效之处在于整个 get 过程不需要加锁,除非读到的值是空才会加锁重读。

put 操作

由于 put 方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put 方法首先定位到 Segment, 然后再 Segment 里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放在 HashEntry 数组里。

size 操作

如果要统计整个 ConcurrentHashMap 里元素的大小,就必须统计所有 Segment 里元素的大小后求和。因为在累加每个 Segment 的 count 的过程中,之前累加过的 count 发生变化的几率非常小,所以 ConcurrentHashMap 的做法是先尝试 2 次通过不锁住 Segment 的方式来统计各个 Segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有 Segment 的大小。

参考

《Java 并发编程的艺术》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值