HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value)
 
本文所有源码都是基于JDK1.8的,不同版本的代码差异可以自行查阅官方文档。
HashMap源码(JDK1.8):
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ....
}
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
//....
}

HashMap 内部存储使用了一个 Node 数组(默认大小是16),每个Node都是一个链表。每个链表存储相同索引的元素。

之所以采取这样的数据结构存储数据是为了防止冲突发生:Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产生。
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

从Java 8开始,HashMap(ConcurrentHashMap以及LinkedHashMap)在处理频繁冲突时,为了提升性能将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值(TREEIFY_THRESHOLD )便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。

而对HashMap进行split操作而生成元素数量在特定的值或以下时,平衡树会被重新转化成链表。
 
HashMap的自动扩容机制
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap 内部的 Node 数组默认的大小是16(DEFAULT_INITIAL_CAPACITY )。

假设有1万个元素需要放入HashMap,那么最好的情况下每个 hash 桶里都有625个元素(每625个元素共用一个索引)。此时你要调用put()、get()、remove()等方法去操作某一个元素,平均要遍历313个元素,效率大大降低。
为了解决这个问题,HashMap 提供了自动扩容机制,当元素个数达到 数组大小 × 负载因子 的数量后会扩大数组的大小(最长链表的Entry个数 > threshold)。在默认情况下,数组大小为16,因子(DEFAULT_LOAD_FACTOR )为0.75,也就是说当 HashMap 中的元素超过16*0.75=12时,会把数组大小扩展为2*16=32,并且重新分配索引,计算每个元素在新数组中的位置。
 
线程不安全
HashMap 在并发时可能出现的问题主要是两方面:
1. put的时候导致的多线程数据不一致
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2. resize而引起死循环(JDK1.8已经不会出现该问题)
这种情况发生在JDK1.7 中HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。
 
线程安全的Map
  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();

Hashtable (deprecate)

Hashtable 源码中是使用 synchronized 来保证线程安全的,比如下面的 get 方法和 put 方法:
public synchronized V get(Object key) {...}
public synchronized V put(K key, V value) {...}

所以当一个线程访问 HashTable 的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。因此Hashtable效率很低,基本被废弃。

ConcurrentHashMap
ConcurrentHashMap沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
且与hashtable不同的是:
ConcurrentHashMap没有对整个hash表进行锁定,而是采用了分离锁(segment)的方式进行局部锁定。具体体现在,它在代码中维护着一个segment数组。
/** For serialization compatibility. */
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
它增加了一个的属性——sizeCtl:
hash表初始化或扩容时的一个控制位标识量。
    负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
在ConcurrentHashMap的Node内部类中,它对val和next属性设置了volatile同步锁,不允许调用setValue方法直接改变Node的value域,增加了find方法辅助map.get()方法。
 
SynchronizedMap
SynchronizedMap是Collectionis的内部类。
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
} public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
public void putAll(Map<? extends K, ? extends V> map) {
synchronized (mutex) {m.putAll(map);}
}
public void clear() {
synchronized (mutex) {m.clear();}
}
//...
}
在 SynchronizedMap 类中使用了 synchronized 同步关键字来保证对 Map 的操作是线程安全的。
三者的效率对比:
分别通过三种方式创建 Map 对象,使用 ExecutorService 来并发运行5个线程,每个线程添加/获取500K个元素,比较其用时多少。
代码就不贴了,详见这里
ConcurrentHashMap明显优于Hashtable和SynchronizedMap 。
 

浅谈HashMap与线程安全 (JDK1.8)的更多相关文章

  1. Java重点之小白解析--浅谈HashMap与HashTable

    这是一个面试经常遇到的知识点,无论什么公司这个知识点几乎是考小白必备,为什么呢?因为这玩意儿太特么常见了,常见到你写一百行代码,都能用到好几次,不问这个问哪个.so!本小白网罗天下HashMap与Ha ...

  2. 浅谈HashMap的实现原理

    1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ...

  3. 浅谈HashMap的内部实现

    权衡时空 HashMap是以键值对的方式存储数据的. 如果没有内存限制,那我直接用哈希Map的键作为数组的索引,取的时候直接按索引get就行了,可是地价那么贵,哪里有无限制的地盘呢. 如果没有时间限制 ...

  4. 转:浅谈SimpleDateFormat的线程安全问题

    转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...

  5. JAVA容器-浅谈HashMap的实现原理

    概述 HashMap是通过数组+链表的方式实现的,由于HashMap的链表也是采用数组方式,我就修改直接利用LinkedList实现,简单模拟一下. 1.Key.Value的存取方式. 2.HashM ...

  6. 浅谈java中线程和操作系统线程

    在聊线程之前,我们先了解一下操作系统线程的发展历程,在最初的时候,操作系统没有进程线程一说,执行程序都是串行方式执行,就像一个队列一样,先执行完排在前面的,再去执行后面的程序,这样的话很多程序的响应就 ...

  7. 【JDK源码分析】浅谈HashMap的原理

    这篇文章给出了这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key ...

  8. struts 2学习笔记—浅谈struts的线程安全

    Sruts 2工作流程: Struts 1中所有的Action都只有一个实例,该Action实例会被反复使用.通过上面Struts 2 的工作流程的红色字体部分我们可以清楚看到Struts 2中每个A ...

  9. 【Java】浅谈HashMap

    HashMap是常用的集合类,以Key-Value形式存储值.下面一起从代码层面理解它的实现. 构造方法 它有好几个构造方法,但几乎都是调此构造方法: public HashMap(int initi ...

随机推荐

  1. VC++ 6.0如何创建与调用动态链接库

    原文:http://blog.csdn.net/wanghaihao_1/article/details/51098451 1.静态链接库与动态链接库区别: 静态链接库:lib中的指令被直接包含在最终 ...

  2. SQLite入门(二)读写二进制数据

    //读二进制数据的函数 BOOL OpenBinDataFile(BYTE **pBUf,UINT &len) {     if (pBUf == NULL)     {         re ...

  3. C++ inheritance: public, private. protected ZZ

    公有继承(public).私有继承(private).保护继承(protected)是常用的三种继承方式. 1. 公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时, ...

  4. gulp 前端构建工具入门

    gulp 前端构建工具入门 标签(空格分隔): gulp 1. 安装gulp npm i -g gulp 2. 创建gulp项目 2.1 Hello world 使用npm init初始化项目文件夹. ...

  5. GetDIBits 提示堆栈损坏的解决办法

    ...... BITMAPINFOHEADER bi; bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = bmpScreen.bmWidth; bi ...

  6. 重新认识KCP

    什么是KCP KCP是一种网络传输协议(ARQ,自动重传请求),可以视它为TCP的代替品,但是它运行于用户空间,它不管底层的发送与接收,只是个纯算法实现可靠传输,它的特点是牺牲带宽来降低延迟.因为TC ...

  7. sql server alter column 由于一个或多个对象访问此列,ALTER TABLE ALTER COLUMN 失败

    今天在修改一个字段类型,由原来的 varchar(500) 增加到 varchar(1000) 则对应的SQL 语句,执行后如图错误 结果查下来是因为其中一个视图建成了索引视图, 解决办法:先将该视图 ...

  8. zt 李鸿章听过《彩云追月》?

    ZT 李鸿章听过<彩云追月>? 2013-05-11 09:35:52 编辑 删除 归档在 观世上之音 | 浏览 1041 次 | 评论 1 条 尝试在网上找一点有关<彩云追月> ...

  9. LINUX基础了解

  10. SAP Fiori + Vue = ?

    2017年3月28日,我到国内一个SAP CRM客户那里,同他们的架构师关于二次开发的UI框架选择SAP UI5还是Vue进行了一番探讨.回到SAP研究院之后,我把这个问题扔到了公司的微信群里,引起了 ...