【小家java】HashMap原理、TreeMap、ConcurrentHashMap的原理、性能、安全方面大解析-----看这一篇就够了(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【小家java】HashMap原理、TreeMap、ConcurrentHashMap的原理、性能、安全方面大解析-----看这一篇就够了(下)

containsValue() 的作用是判断HashMap是否包含“值为value”的元素。


public boolean containsValue(Object value) {
    // 若“value为null”,则调用containsNullValue()查找
    if (value == null)
        return containsNullValue();
    // 若“value不为null”,则查找HashMap中是否有值为value的节点。
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;
}


HashMap实现了Cloneable接口,即实现了clone()方法。


clone()方法的作用很简单,就是克隆一个HashMap对象并返回。


// 克隆一个HashMap,并返回Object对象
public Object clone() {
    HashMap<K,V> result = null;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // assert false;
    }
    result.table = new Entry[table.length];
    result.entrySet = null;
    result.modCount = 0;
    result.size = 0;
    result.init();
    // 调用putAllForCreate()将全部元素添加到HashMap中
    result.putAllForCreate(this);
    return result;
}


关于HashMap在Java8中的实现,我推荐参考这篇文章:HashMap在java8中的原理


HashTable的原理


基本和HashMap一样,只是它所有的方法都是被synchronized修饰的,包括toString()方法,所以效率是很低的,基本不会再使用它了。


TreeMap的原理

TreeMap的实现是红黑树算法的实现,所以需要了解TreeMap的原理,需要了解红黑树的原理,这里推荐红黑树原理

所以了解TreeMap的put、get方法的原理,其实都是需要深入了解红黑树对节点的处理。


LinkedHashMap 的原理


HashMap和双向链表合二为一即是LinkedHashMap。所谓LinkedHashMap,其落脚点在HashMap,因此更准确地说,它是一个将所有Entry节点链入一个双向链表的HashMap。由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap的元素存取过程基本与HashMap基本类似,只是在细节实现上稍有不同。当然,这是由LinkedHashMap本身的特性所决定的,因为它额外维护了一个双向链表用于保持迭代顺序。此外,LinkedHashMap可以很好的支持LRU算法。


虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。


更具体的解释,我给导向到这里:Map 综述(二):彻头彻尾理解 LinkedHashMap


1.HashMap 是基于“拉链法”实现的散列表。一般用于单线程程序中。

2.Hashtable 也是基于“拉链法”实现的散列表。它一般用于多线程程序中。

3.WeakHashMap 也是基于“拉链法”实现的散列表,它一般也用于单线程程序中。相比HashMap,WeakHashMap中的键是“弱键”,当“弱键”被GC回收时,它对应的键值对也会被从WeakHashMap中删除;而HashMap中的键是强键。

4. TreeMap 是有序的散列表,它是通过红黑树实现的。它一般用于单线程中存储有序的映射。

5. LinkedHashMap:存储需要保证插入顺序的单线程环境中


HashMap为什么线程不安全?有什么影响?


一直以来都知道HashMap是线程不安全的,但是到底为什么线程不安全,在多线程操作情况下什么时候线程不安全?


让我们先来了解一下HashMap的底层存储结构,HashMap底层是一个Entry数组,一旦发生Hash冲突的的时候,HashMap采用拉链法解决碰撞冲突,Entry内部的变量:


final Object key;  
Object value;  
Entry next;  
int hash; 

通过Entry内部的next变量可以知道使用的是链表,这时候我们可以知道,如果多个线程,在某一时刻同时操作HashMap并执行put操作,而有大于两个key的hash值相同,如图中a1、a2,这个时候需要解决碰撞冲突,而解决冲突的办法上面已经说过,对于链表的结构在这里不再赘述,暂且不讨论是从链表头部插入还是从尾部初入,这个时候两个线程如果恰好都取到了对应位置的头结点e1,而最终的结果可想而知,a1、a2两个数据中势必会有一个会丢失,如图所示:


image.png


看看put方法:

public Object put(Object obj, Object obj1)  
    {  
        if(table == EMPTY_TABLE)  
            inflateTable(threshold);  
        if(obj == null)  
            return putForNullKey(obj1);  
        int i = hash(obj);  
        int j = indexFor(i, table.length);  
        for(Entry entry = table[j]; entry != null; entry = entry.next)  
        {  
            Object obj2;  
            if(entry.hash == i && ((obj2 = entry.key) == obj || obj.equals(obj2)))  
            {  
                Object obj3 = entry.value;  
                entry.value = obj1;  
                entry.recordAccess(this);  
                return obj3;  
            }  
        }  
        modCount++;  
        addEntry(i, obj, obj1, j);  
        return null;  
    }  


put方法不是同步的,同时调用了addEntry方法。addEntry方法依然不是同步的,所以导致了线程不安全出现伤处问题,其他类似操作不再说明,源码一看便知,下面主要说一下另一个非常重要的知识点,同样也是HashMap非线程安全的原因,我们知道在HashMap存在扩容的情况,对应的方法为HashMap中的resize方法:

void resize(int i)  
    {  
        Entry aentry[] = table;  
        int j = aentry.length;  
        if(j == 1073741824)  
        {  
            threshold = 2147483647;  
            return;  
        } else  
        {  
            Entry aentry1[] = new Entry[i];  
            transfer(aentry1, initHashSeedAsNeeded(i));  
            table = aentry1;  
            threshold = (int)Math.min((float)i * loadFactor, 1.073742E+009F);  
            return;  
        }  
    }  


可以看到扩容方法也不是同步的,通过代码我们知道在扩容过程中,会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。

**当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作**,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。


使用线程安全的Map


HashTable或者Collections.synchronizedMap


但是这两位选手都有一个共同的问题:性能。因为不管是读还是写操作,他们都会给整个集合上锁,导致同一时间的其他操作被阻塞。

虽然HashTable和Collections.synchronizedMap解决了HashMap的线程不安全的问题,但是带来了运行效率不佳的问题。

基于以上所述,兼顾了线程安全和运行效率的ConcurrentHashMap就出现了。


Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步.


ConcurrentHashMap:实现线程安全的HashMap


ConcurrentHashMap与HashMap相比,最关键的是要理解一个概念:segment。

Segment其实就是一个Hashmap 。Segment也包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。

Segment对象在ConcurrentHashMap集合中有2的N次方个,共同保存在一个名为segments的数组当中。(类比HashMap来理解Segment就好)因此ConcurrentHashMap的结构为:


image.png



换言之,ConcurrentHashMap是一个双层哈希表。在一个总的哈希表下面,有若干个子哈希表。(这样的双层结构,类似于数据库水平拆分来理解)ConcurrentHashMap如此的设计,优势主要在于: 每个segment的读写是高度自治的,segment之间互不影响。这称之为**“锁分段技术”**;

看一下并发情况下的ConcurrentHashMap:

– 情景一:不同segment的并发写入


image.pngimage.pngimage.png

image.png



不同的Segment是可以并发执行put操作的


– 情景二:同一segment的并发写入


image.png


因为segment的写入是上锁的,因此对 同一segment的并发写入会被阻塞;


– 情景三:同一segment的一写一读

image.png


同一segment的写和读是可以并发执行的;


下面简要说说读写的过程:

get:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.再次通过hash值,定位到Segment当中数组的具体位置。

put:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.获取可重入锁

4.再次通过hash值,定位到Segment当中数组的具体位置。

5.插入或覆盖HashEntry对象。

6.释放锁。


抛出一个问题:每一个segment各自持有锁,那么在调用size()方法的时候(size()在实际开发大量使用),怎么保持一致性呢?


Size方法的目的是统计ConcurrentHashMap的总元素数量, 肯定要把每个segment内部的元素数量都加起来。


那么假设一种情况,在统计segment元素数量的过程中,在统计结束前,已统计过的segment插入了新的元素,size()返回的数量就会出现不一致的问题。

为解决这个问题,ConcurrentHashMap的Size()方法是通过一个嵌套循环解决的,大体过程如下:


1.遍历所有的Segment。

2.把Segment的元素数量累加起来。

3.把Segment的修改次数累加起来。

4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。

5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。

6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

7.释放锁,统计结束。


为了不锁所有segment,首先乐观地假设size过程中不会有修改。当尝试一定次数,才无奈转悲观,锁住所有segment以保证一致性。

以上都是基于Java1.7的ConcurrentHashMap原理和代码;ConcurrentHashMap在对Key求Hash值的时候进行了两次Hash,目的是为了实现Segment均匀分布。


jdk1.7中采用Segment + HashEntry的方式进行实现,结构如下:


image.png



1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:


image.png


只有在执行第一次put方法时才会调用initTable()初始化Node数组,实现如下:


private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}


1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;


HashTable使用一把锁处理并发问题,当有多个线程访问时,需要多个线程竞争一把锁,导致阻塞


相关文章
|
9天前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
47 13
|
1月前
|
XML Java 数据库连接
性能提升秘籍:如何高效使用Java连接池管理数据库连接
在Java应用中,数据库连接管理至关重要。随着访问量增加,频繁创建和关闭连接会影响性能。为此,Java连接池技术应运而生,如HikariCP。本文通过代码示例介绍如何引入HikariCP依赖、配置连接池参数及使用连接池高效管理数据库连接,提升系统性能。
62 5
|
1月前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
33 4
|
1月前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
41 1
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
79 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
69 0
|
2天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
2天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

推荐镜像

更多