前言

(注,本文写自深夜,有点懵,所以想到哪里,就写到哪里,格式排版什么的,以后再改)
环境:openjdk 14
本文主要内容:

  • 介绍 Hashtable 与 ConcurrentHashMap 实现线程安全版 HashMap 的方式
  • 介绍 Hashtable fail-fast 模式与非 fail-fast 模式

HashTable

线程安全的实现方式

对暴露的方法,除了构造函数外,均使用 synchronized 修饰
image.png

fail-fast 与 非 fail-fast

看到网上有些文章有写 Hashtable 是 fail-safe 模式,我觉得这个说法有问题。首先简要介绍下什么是 fail-fast。我觉得 fail-fast 用于避免在遍历该数据结构时,有其他线程对该结构进行了修改。如何实现的?使用modCount记录结构修改次数,每次修改,例如增加,删除,modCount就会+1,在获取迭代器实例时,会使用 expectedModCount 记录当前的 modCount 值
image.png
在使用迭代器进行遍历时,每次获取下一个节点值前,expectedModCount 都会与当前的 modCount 进行比较,如果不相等则说明有线程进行了修改,则停止遍历,直接抛出异常
image.png
注意,这个抛出异常并不是为了解决线程安全,而只是用于测试,告知当前有线程安全问题,存在 bug
image.png
来验证一下,其实也不需要在多线程环境下,在获取迭代器后修改一下结构就可以了

  1. package cn.zjm404.stu.java.hashtable;
  2. import java.util.Hashtable;
  3. public class ForeachDemo {
  4. public static void main(String[] args) {
  5. Hashtable<String, String> hashtable = new Hashtable<>();
  6. hashtable.put("k1","hello");
  7. hashtable.put("k2","world");
  8. hashtable.put("k3","HELLO");
  9. hashtable.put("k4","WORLD");
  10. for (String s : hashtable.keySet()) {
  11. if (hashtable.containsKey("k3")){
  12. hashtable.remove("k3");
  13. }
  14. System.out.println(s);
  15. }
  16. }
  17. }

抛出了异常
image.png
所以,并不能说 Hashtable 为 fail-safe模式,它也是支持 fail-fast 模式的。那么它是否支持 fail-safe 模式?这里再介绍一下 fail-safe 模式,简单的说就是即时在迭代器遍历时,结构被修改了,也不会影响当前的遍历。如何实现的呢?就是在更新前先拷贝当前的结构,然后在这个拷贝的结构中进行修改,就不会影响正在使用迭代器遍历的行为。就是 copy-on-write。可以参考下 CopyOnWriteArrayList 的迭代器来看下
COWIterator 用 snapshot 来保存获取迭代器前的结构
image.png
snapshot 的值来自于 array
image.png
image.png
当进行修改时,在拷贝的数组中进行修改,修改完成,再更新 array 引用当前的拷贝数组,因为并没有修改之前的数组中的元素,所以并不会影响到遍历的行为(感觉这里可以抽出来单独作为一篇讲解COW 的博客来着…)
image.png
找了一下,Hashtable 并没有这样的结构。并且注释里也有点出 Enumerations 返回的不是 fail-fast 模式的迭代器,但是也没说是 fail-safe,只是不会抛出 java.util.ConcurrentModificationException 而已。而且从职责上也可以推断出,COW 并不能保证获取最新信息,这对于要求信息更新及时的 Hashtable 来讲,是无法接受的。
image.png
那么为什么不需要抛出这个异常呢?我觉得是因为散列表本就是无序的,因为不需要维护节点间的关系,因此,就算在遍历的时候结构发生了改变,也不会造成重复读取数据和漏读数据的情况。迭代器如下,即使节点被删除了,也不影响遍历剩下的节点。会将其跳过,继续遍历。注意,这里获取的是 key,遍历 key。因为这里遍历时判断是否跳过的条件有 e == null,所以这就是为什么 hashtable 的 key 不可以为 null 的原因
image.png

基于这个迭代器,来试验一下~这里直接输出的是 value

  1. package cn.zjm404.stu.java.hashmap;
  2. import java.util.HashMap;
  3. public class HashTest {
  4. public static void main(String[] args) {
  5. HashMap<String, String> map = new HashMap<>();
  6. map.put("key1","hello world");
  7. }
  8. }

image.png
返回的是 value 主要是因为这个
image.png

ConcurrentHashMap

HashTable 虽然也可以保证线程安全,但是synchronized 是修饰方法,锁为当前资源对象,当线程获取锁后,对当前数据结构,其它线程无法进行任何操作,哪怕修改的不是同一个桶也不行,锁的粒度太大了。因此就有了 ConcurrentHashMap,将粒度缩小到了桶的范围。如下,先找到映射的桶,然后如果有对桶进行的操作,则使用 内部锁,锁住这个桶,然后再进行操作,就这样将粒度缩小到了桶的范围
image.png
(之后的,以后再写,睡觉~~)