浅谈Java并发之从HashMap到ConcurrentHashMap

Map

这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。
本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的

ConcurrentHashMap

HashMap
众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

JDK1.7 中的数据结构图:

在这里插入图片描述
先看一下JDK1.7中:
在这里插入图片描述
这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?

1.初始化桶大小,因为底层是数组,所以这是数组默认的大小。
2.桶最大值。
3.默认的负载因子(0.75)
4.table 真正存放数据的数组。
5.Map 存放数量的大小。
6.桶大小,可在初始化时显式指定。
7.负载因子,可在初始化时显式指定。

重点解释下负载因子: 由于给定的 HashMap 的容量大小是固定的,比如默认初始化:

 1    public HashMap() {
   
 2        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
 3    }
 4
 5    public HashMap(int initialCapacity, float loadFactor) {
   
 6        if (initialCapacity < 0)
 7            throw new IllegalArgumentException("Illegal initial capacity: " +
 8                                               initialCapacity);
 9        if (initialCapacity > MAXIMUM_CAPACITY)
10            initialCapacity = MAXIMUM_CAPACITY;
11        if (loadFactor <= 0 || Float.isNaN(loadFactor))
12            throw new IllegalArgumentException("Illegal load factor: " +
13                                               loadFactor);
14
15        this.loadFactor = loadFactor;
16        threshold = initialCapacity;
17        init();
18    }

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。

根据代码可以看到其实真正存放数据的是

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

这个数组,那么它又是如何定义的呢?

在这里插入图片描述
Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:
key 就是写入时的键。
value 自然就是值。
开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
hash 存放的是当前 key 的 hashcode。

不知道 1.7 的实现大家看出需要优化的点没有? 其实一个很明显的地方就是:

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

因此 1.8 中重点优化了这个查询效率。
1.8 HashMap 结构图:

在这里插入图片描述

HashMap在jdk8中相较于jdk7在底层实现方面的不同:

1. new HashMap():底层没创建一个长度为16的数组
2. jdk 8底层的数组是:Node[],而非Entry[]
3. 首次调用put()方法时,底层创建长度为16的数组
4. jdk7底层结构:数组+链表。jdk8中底层结构:数组+链表+红黑树。
4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

这里强调一下HashMap的遍历方式:

在这里插入图片描述
**

HashMap 遍历从大的方向来说,可分为以下 4 类:

1.迭代器(Iterator)方式遍历;
2.For Each 方式遍历;
3.Lambda 表达式遍历(JDK 1.8+);
4.Streams API 遍历(JDK 1.8+)。

但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

1.使用迭代器(Iterator)EntrySet 的方式进行遍历;
2.使用迭代器(Iterator)KeySet 的方式进行遍历;
3.使用 For Each EntrySet 的方式进行遍历;
4.使用 For Each KeySet 的方式进行遍历;
5.使用 Lambda 表达式的方式进行遍历;
6.使用 Streams API 单线程的方式进行遍历;
7.使用 Streams API 多线程的方式进行遍历。
**

这里说两种常用的遍历方法:

//方式一: entrySet()
Set entrySet = map . entrySet();
Iterator iterator1 = entrySet . iterator();
while (iterator1. hasNext()){
   
Object obj = iterator1.next();
    //entrySet集合中的元素都是entry
Map. Entry lIntry = (Map.Entry) obj;
System. out. print1n(entry. getKey() +---->" + entry. getValue());
}
System. out . print1n();



//方式二:
Set keySet = map. keySet();
Iterator iterator2 = keySet . iterator();
while(iterator2. hasNext()){
   
Object key = iterator2. next();
Object value = map. get(key);
System. out . println(key + "=====" + value);


还有几种我后面会专门写一篇HashMap的不同遍历方法分析。

下面来说说ConcurrentHashMap

在Java1.5中,并发编程大师Doug Lea给我们带来了concurrent包,而该包中提供的ConcurrentHashMap是线程安全并且高效的HashMap,本节我们就来研究下ConcurrentHashMap是如何保证线程安全的同时又能高效的操作。
1.为何用ConcurrentHashMap
在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下。
线程不安全的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++) {
   
     
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值