区分HashMap、HashTable、CurrentHashMap三者的区别

1. 先从HashMap和HashTable讲起:
两者都实现了Map接口,主要区别在于线程安全性,同步(synchronization),以及速度
1.1. HashMap的KV均可为null,而HashTable的KV均不能为null。因此,不是使用get(key)判断HashMap是否存在某个键,而应该用containsKey(key)来判断某个键
1.2. HashMap是非synchronized,而HashTable则是synchronied(这也意味着同一时刻,只能有一个线程获取同步锁,对HashTable的值进行修改)。这意味着HashTable线程安全,而HashMap则不是线程安全的。
1.3. HashMap的迭代器是fail-fast(快速失败)迭代器,而HashTable则是fail-safe(安全失败)迭代器。二者的主要区别在于遍历集合时,是否能同时修改集合的元素。
fail-fast机制是在遍历一个集合时,集合元素不能被改动,否则抛Concurrent Modification Exception异常(注意:这种机制在多线程情况下可能会有集合元素发生改动,但未被检查出错误的情况发生。fail-fast只能说是尽力去发现改动并抛出异常,因此此类迭代器应该仅适用于检测bug)
fail-safe机制是任何对集合结构的修改都会在另一个此集合的复制品上体现,因此不会抛Concurrent Modification Exception异常。因此该机制也有一定缺陷(1. 需要复制集合 2. 无法保证读取的数据是原集合中的数据,即可能出现在线程A遍历集合的过程中线程B,对集合中某一元素进行了修改,导致线程A可能读取到线程B所修改的数据,而原始的数据)
1.4. 由HashTable是线程安全和synchronized的特性,决定了在单线程环境下它势必要比HashMap的运行速度要慢
1.5. HashTable和HashMap的底层实现均是数组 + 链表
HashTable的初始化 size = 11,扩容 newsize = oldsize2 + 1,
计算index的方式是 index = (hash & 0x7FFFFFFF) % tab.length。
HashMap的初始化 szie = 16, 扩容 newsize = oldsize
2(size是2的 n 次方),
计算index的方式是 index = hash & (tab.length – 1)
1.6. HashMap和HashTable都是插入后扩容,因此有可能产生无效扩容(扩容后不再插入导致空间的浪费),且每次扩容,原来数组中的元素都要重新计算其存放的位置后重新插入。扩容触发条件是当前有效存储数组的hash值个数超过总容量的75%(负载极限,默认值为75%,可调)时自动触发

2. HashMap如何解决冲突:
若干key的哈希值按数组大小取模后,若落在同一个数组下标上,会组成一条存储KV的实体链。当要从HashMap中通过key取出具体某个具体值时,首先就需要计算Hash值获取具体坐标,而后再通过equals()方法在实体链上进行逐一比较,以获得具体的value值
注:若存储链表的长度超过阈值(默认8),链表会被改造成树形结构

JDK5之后提供了ConcurrentHashMap来替代HashTable。ConcurrentHashMap是对HashTable的优化。
3. ConcurrentHashMap(采用分段锁,线程安全)
3.1. 底层实现采用 分段数组+链表
3.2. 默认将整个Map分成16片(Segment,可调参数)
3.3. HashTable进行修改操作时,要锁住整张表,以保证数据安全;而ConcurrentHashMap只需锁住要修改数据所在的那一分片(锁分离技术)
3.4. 有时候某些操作需要涉及到多个分片,如获取Map的size、查找某个值是否存在,此时只需要按顺序将每一片锁住,操作完毕后,再按顺序将锁释放
3.5. ConcurrentHashMap是段内插入前扩容,即插入前检查当前有效存储数组的hash值个数是否超过负载因子(默认为总容量的75%,可调参数),若超过,进行段内扩容

4. 分段锁技术

4.1. 将数据分成若干段进行存储
4.2. 为每一段分配一把锁
4.3. 当线程需要对某一段进行增删改查时,需要先获取分段锁,再进行操作,操作完毕再释放

猜你喜欢

转载自blog.csdn.net/weixin_43247186/article/details/87215344