ConcurrentHashMap的实现原理与使用

什么是ConcurrentHashMap?

ConcurrentHashMap 是java集合中map的实现,是哈希表的线程安全版本,即使是线程安全版本,

ConcurrentHashMap的性能也十分可观。但是在不同的jdk版本中,其实现也不一样,本文主要基于jdk1.8版本的实现讨论。

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

 

为什么要使用ConcurrentHashMap?

1 线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)

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

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

 

ConcurrentHashMap 结构

  ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成。

  Segment 是一种可重入锁(ReentrantLock),在ConcurrentHashMap 里扮演锁的角色。

  一个ConCurrentHashMap里包含一个 Segment数组。

 Segment的结构和HashMap 类似,是一种数组和链表结构。

 HashEntry 则用于存储键值 对数据。

 

初始化

 ConcurrentHaspMap初始化方法是通过initialCapacity,loadFactor, concurrencyLevel几个参数来初始化segments数组,段偏移量segmentShift,段掩码segmentMask和每个segment里的HashEntry数组

 

为了能通过按位与的哈希算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个是大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度。假如concurrencyLevel等于14,15或16,ssize都会等于16,即容器里锁的个数也是16

注意concurrencyLevel的最大大小是65535,意味着segments数组的长度最大为65536,对应的二进制是16位

 

ConcurrentHaspMap 操作

get操作(不加锁)

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

  思路:  (1)确定键值对在哪个段

                (2)确定键值对在哪个小的链表上 tab[index]

                 (3)遍历链表,找到指定的key

get方法步骤:
1、计算key的hash值,并定位table索引
2、若table索引下元素(head节点)为普通链表,则按链表的形式迭代遍历。
3、若table索引下元素为红黑树TreeBin节点,则按红黑树的方式查找(find方法)。

 

put操作 添加键值对.(加锁)

由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作

size操作(先尝试不加锁,再尝试加锁)

如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。

 

实列:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class MyConcurrentMashMapTest {



public static void main(String[] arg)
{
//ConcurrentHashMap 是java集合中map的实现,是哈希表的线程安全版本,即使是线程安全版本
Map<String,Integer> count=new ConcurrentHashMap<>();
//CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值
CountDownLatch endLatch = new CountDownLatch(3);

Runnable runnable=new Runnable() {
@Override
public void run() {
Integer oldvalue;
for(int i=0;i<5;i++)
{
//
Integer value = count.get("123");
if (null == value) {
// 添加键值对
count.put("123", 1);
} else {

count.put("123", value + 1);
}

}
//countDown 的时候每次调用都会对 state 减 1 也就是我们
// new CountDownLatch(3); 的这个计数器的数字减 1
endLatch.countDown();

}
};

//new CountDownLatch(3); 3 代表 3个线程
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();

try {
//此方法用来让当前线程阻塞,直到count减小为0才恢复执行,await 方法它会去获取同步值发现为
// 0 的话成功返回,如果小于 0 的话,再次判断是否是头结点
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}


}
}

结果:{123=15}

1. ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

2.我们可以使用CocurrentHashMap来代替Hashtable吗?
我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

3. ConcurrentHashMap有什么缺陷吗?
ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。

4. ConcurrentHashMap在JDK 7和8之间的区别

  • JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  • JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

 

总结:

1.ConcurrentHashMap的数据结构与HashMap基本相同,只是在put的过程中如果没有发生冲突,则采用CAS操作进行无锁化更新,只有发生了哈希冲突的时候才锁住在链表上添加新Node或者更新Node的操作。

2.像get一类的操作也是没有同步的。

3.ConcurrentHashMap 不允许存放null值。

4.ConcurrentHashMap 的大小是通过计算出来的,也就是说在超高的并发情况下,size是不精确的。这一点后面有空再补上。

5.和jdk1.7 相比,在jdk1.7中采用锁分段技术,更加复杂一点,jdk1.8中ConcurrentHashMap上锁仅在发生hash冲突时才上锁,且仅影响发生冲突的那一个链表的更新操作。

posted on 2020-08-14 17:23  shumeigang  阅读(11814)  评论(1编辑  收藏  举报

导航