Hashtable和ConcurrentHashMap

HashMap如何保证线程安全?

一般有三种方式来代替原生的线程不安全的 HashMap:

1)使用 java.util.Collections 类的 synchronizedMap 方法包装一下 HashMap,得到线程安全的 HashMap,其原理就是对所有的修改操作都加上 synchronized。方法如下:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 

2)使用线程安全的 Hashtable 类代替,该类在对数据操作的时候都会上锁,也就是加上 synchronized

3)使用线程安全的 ConcurrentHashMap 类代替,该类在 JDK 1.7 和 JDK 1.8 的底层原理有所不同,JDK 1.7 采用数组 + 链表存储数据,使用分段锁 Segment 保证线程安全;JDK 1.8 采用数组 + 链表/红黑树存储数据,使用 CAS + synchronized 保证线程安全。

不过**前两者的线程并发度并不高,容易发生大规模阻塞,**所以一般使用的都是 ConcurrentHashMap,他的性能和效率明显高于前两者

synchronizedMap具体是如何保证线程安全?

一般我们会使用synchronizedMap来创建一个线程安全的Map

Map m = Collections.synchronizedMap(new HashMap(...));

Collections 中的这个静态方法 synchronizedMap 其实是创建了一个内部类的对象,这个内部类就是 SynchronizedMap。在其**内部维护了一个普通的 Map 对象以及互斥锁 mutex,**如下图所示:
在这里插入图片描述
可以看到 SynchronizedMap 有两个构造函数,如果你传入了互斥锁 mutex 参数,就使用我们自己传入的互斥锁。如果没有传入,则将互斥锁赋值为 this,也就是将调用了该构造函数的对象作为互斥锁,即我们上面所说的 Map。

创建出 SynchronizedMap 对象之后,通过源码可以看到对于这个对象的所有操作全部都是上了悲观锁 synchronized 的:
在这里插入图片描述
由于多个线程都共享同一把互斥锁,导致同一时刻只能有一个线程进行读写操作,而其他线程只能等待,所以虽然它支持高并发,但是并发度太低,多线程情况下性能比较低下。
而且,大多数情况下,业务场景都是**读多写少,**多个线程之间的读操作本身其实并不冲突,所以SynchronizedMap 极大的限制了读的性能。
所以多线程并发场景我们很少使用 SynchronizedMap 。

为什么不使用HashTable

和 SynchronizedMap 一样,Hashtable 也是非常粗暴的给每个方法都加上了悲观锁 synchronized,我们随便找几个方法看看:
在这里插入图片描述

除了加锁,Hashtable和HashMap还有不同吗?

Hash table的key和value不支持null,但是HashMap是支持key和value为null的
1)如果我们 put 了一个 value 为 null 进入 Map,Hashtable 会直接抛空指针异常:
在这里插入图片描述
2)如果我们put一个key为null进入Map,在调用下面这个方法时就会报错,因为我们使用null值去调用方法了
在这里插入图片描述
那么HashMap为什么支持key和value为null呢?
1)HashMap 相比 Hashtable 做了一个特殊的处理,如果我们put进来的key是null,那么HashMap在计算这个key的hash值时会直接返回0
在这里插入图片描述
也就是说 HashMap 中 key为 null 的键值对的 hash 为 0。因此一个 HashMap 对象中只会存储一个 key 为 null 的键值对,因为它们的 hash 值都相同。
2)因为HashMap不会对put进来的value作检验,所以如果我们put进来的value值为null也没关系,因此一个HashMap对象可以存储多个value为null的键值对
在这里插入图片描述
但是有一个点要注意,并不是说调用get方法返回null就代表这个值在HashMap里,我们来看一下源码
在这里插入图片描述
如果 Map 中没有查询到这个 key 的键值对,那么 get 方法就会返回 null 对象。但是我们上面刚刚说了,HashMap 里面可以存在多个 value 为 null 的键值对,也就是说,通过 get(key) 方法返回的结果为 null 有两种可能:
1)没有查询到对应的键值对,返回null
2)HashMap 中这个 key 对应的 value 为 null
因此我们不能使用get方法来判断HashMap是否存在某个key,而是应该使用containsKey
方法。

为什么Hashtable不支持 key 和 value 为 null 呢?

不仅仅Hashtable不支持key和value为null,ConcurrentHashMap也不支持,作为支持并发的容器,如果它们像 HashMap 一样,允许 null key 和 null value 的话,在多线程环境下会出现问题。
假设它们允许 null key 和 null value,我们来看看会出现什么问题:当你通过 get(key) 获取到对应的 value 时,如果返回的结果是 null 时,你无法判断这个 key 是否真的存在。为此,我们需要调用 containsKey 方法来判断这个 key 到底是 value = null 还是它根本就不存在,如果 containsKey 方法返回的结果是 true,OK,那我们就可以调用 map.get(key) 获取 value。
但是注意,这仅仅是在单线程的情况下!!
由于 Hashtable 和 ConcurrentHashMap 是支持多线程的容器,在调用 map.get(key) 的这个时候 map 对象可能已经不同了
比如说某个线程 A 调用了 map.get(key) 方法,它返回为 value = null 的真实情况就是因为这个 key 不能存在。当然,线程 A 还是会按部就班的继续用 map.containsKey(key),我们期望的结果是返回 false。
但是如果在线程 A 调用 map.get(key) 方法之后,map.containsKey 方法之前,另一个线程 B 执行了 map.put(key,null) 的操作。那么线程 A 调用的 map.containsKey 方法返回的就是 true 了。这就与我们的假设的真实情况不符合了。
所以为了保证并发情况的安全性,Hashtable 和 ConcurrentHashMap 不允许 key 和 value 为 null

Hashtable和HashMap的区别

除了 Hashtable 不允许 null key 和 null value 而 HashMap 允许以外,它俩还有以下几点不同:
1)初始化容量不同:HashMap 的初始容量为 16,Hashtable 初始容量为 11。两者的负载因子默认都是 0.75;

2)扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1;

3)迭代器不同:首先,Hashtable 的迭代器 Iterator是fail-fast(快速失败)的,而HashTable的迭代器Enumerator是fail-safe(失败安全)的

fail-fast原理

当迭代器在遍历容器中的元素时,会维护一个modCount变量,在遍历集合过程中,如果某个元素发生变化,modCount的值也会发生改变。这就是为什么hashMap是线程不安全的。

因为在使用hasNext()/next()方法时,hashMap首先会拿元素的modCount和一个expectedmodCount值比较,如果两者相等就返回这个元素的值,否则返回一个ConcurrentModificationException错误。
(**这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。**如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。这就是为什么hashMap线程不安全)

fail-safe原理

和fail-fast不同,fail-safe机制在遍历集合元素时,如果此时对集合结构修改,**fail-safe机制会先重新复制一份原集合的元素出来,然后迭代器遍历的是这个原集合的副本!**这也是为什么hashTable支持多线程了。但是fail-safe也有缺点:
1.复制原集合需要额外的空间和时间开销
2.无法保证遍历的是最新的内容。

ConcurrentHashMap1.7

存储结构
在这里插入图片描述
Java 7 中 ConcurrentHashMap 的存储结构如上图,ConcurrnetHashMap 由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变默认 Segment 的个数是 16 个,你也可以认为 ConcurrentHashMap 默认支持最多 16 个线程并发。

初始化

通过 ConcurrentHashMap 的无参构造探寻 ConcurrentHashMap 的初始化流程。

    /**
     * Creates a new, empty map with a default initial capacity (16),
     * load factor (0.75) and concurrencyLevel (16).
     */
    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

无参函数中调用了有参构造,总共有三个参数

    /**
     * 默认初始化容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 默认负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 默认并发级别
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

总结一下在 Java 7 中 ConcurrnetHashMap 的初始化逻辑。
1.必要参数校验。
2.校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.
3.寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。
4.记录 segmentShift 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
5.记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
6.初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashTableConcurrentHashMap都是key-value结构的数据存储容器。它们的底层实现原理都差不多,但最大的区别在于线程安全性。 HashTable是线程安全的,而HashMap则不是。这是因为HashTable在每个方法上都加上了悲观锁synchronized来保证线程安全性。悲观锁的使用会导致并发性能下降,因为每个线程在访问容器的时候都需要获取锁。 ConcurrentHashMap是一种高效的线程安全容器。它没有像HashTable那样在每个方法上使用重量级锁,而是使用了乐观锁(CAS)和无锁算法。ConcurrentHashMap在关键位置使用乐观锁,允许线程无阻塞地进行操作。读方法没有加锁,而且在扩容时老数据的转移是并发执行的,从而提高了扩容的效率。 由于ConcurrentHashMap的高效性和线程安全性,一般情况下我们会选择使用ConcurrentHashMap而不是HashTable。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [ConcurrentHashMapHashTable](https://blog.csdn.net/a141210104/article/details/127391379)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [HashtableConcurrentHashMap](https://blog.csdn.net/qq_45725126/article/details/119085871)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值