
HashMap、Hashtable和ConcurrentHashMap的区别
- ✔️ 三者区别
 - ✔️ 线程安全方面
 - ✔️继承关系方面
 - ✔️ 允不允许null值方面
 - ✔️为什么ConcurrentHashMap不允许null值?
 
- ✔️ 默认初始容量和扩容机制
 - ✔️遍历方式的内部实现上不同
 
✔️ 三者区别
✔️ 线程安全方面
HashMap是非线程安全的。
Hashtable 中的方法是同步的,所以它是线程安全的。
ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全,ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好。
ConcurrentHashMap在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized,这个实现被称为"分段锁"的变种,也被称为"锁分离”,它将锁定粒度更细,把锁的粒度从整个Map降低到了单个桶。
看一段代码,HashMap、Hashtable和ConcurrentHashMap在多线程环境中的行为:
import java.util.HashMap;  
import java.util.Hashtable;  
import java.util.concurrent.ConcurrentHashMap;  
  
public class ThreadSafeHashMapComparison {  
  
    public static void main(String[] args) {  
          
        // 1. HashMap - 非线程安全的示例  
        HashMapExample(new HashMap<>());  
          
        // 2. Hashtable - 线程安全的示例  
        HashtableExample(new Hashtable<>());  
          
        // 3. ConcurrentHashMap - 线程安全的示例,性能更好  
        ConcurrentHashMapExample(new ConcurrentHashMap<>());  
    }  
      
    public static void HashMapExample(HashMap<Integer, String> map) {  
        map.put(1, "One");  
        map.put(2, "Two");  
        map.put(3, "Three");  
          
        // 启动两个线程同时修改map  
        new Thread(() -> {  
            map.put(4, "Four");  
        }).start();  
        new Thread(() -> {  
            map.remove(2);  
        }).start();  
    }  
      
    public static void HashtableExample(Hashtable<Integer, String> hashtable) {  
        hashtable.put(1, "One");  
        hashtable.put(2, "Two");  
        hashtable.put(3, "Three");  
          
        // 启动两个线程同时修改hashtable  
        new Thread(() -> {  
            hashtable.put(4, "Four");  
        }).start();  
        new Thread(() -> {  
            hashtable.remove(2);  
        }).start();  
    }  
      
    public static void ConcurrentHashMapExample(ConcurrentHashMap<Integer, String> concurrentHashMap) {  
        concurrentHashMap.put(1, "One");  
        concurrentHashMap.put(2, "Two");  
        concurrentHashMap.put(3, "Three");  
          
        // 启动两个线程同时修改concurrentHashMap  
        new Thread(() -> {  
            concurrentHashMap.put(4, "Four");  
        }).start();  
        new Thread(() -> {  
            concurrentHashMap.remove(2);  
        }).start();  
    }  
}
 
趁热打铁,再来看一段代码,使用Java中的ReentrantLock 和 Condition 来 实现一个线程安全的、可扩展的 HashMap 。这个示例中,我们还将展示如何处理更复杂的并发情况,如多个线程同时尝试修改相同的键。
import java.util.HashMap;  
import java.util.Map;  
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class ThreadSafeHashMap<K, V> {  
  
    private final Map<K, V> map = new HashMap<>();  
    private final ReentrantLock lock = new ReentrantLock();  
    private final Condition condition = lock.newCondition();  
  
    public V put(K key, V value) {  
        lock.lock();  
        try {  
            // 等待当前线程获取锁后,再执行下面的代码  
            condition.await();  
            // 检查键是否已经存在,如果存在则更新值,否则插入新键值对  
            return map.merge(key, value, (oldValue, newValue) -> newValue);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
            throw new RuntimeException(e);  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    public V remove(K key) {  
        lock.lock();  
        try {  
            // 等待当前线程获取锁后,再执行下面的代码  
            condition.await();  
            return map.remove(key);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
            throw new RuntimeException(e);  
        } finally {  
            lock.unlock();  
        }  
    }  
      
    public static void main(String[] args) {  
        ThreadSafeHashMap<Integer, String> threadSafeHashMap = new ThreadSafeHashMap<>();  
        threadSafeHashMap.put(1, "One");  
        threadSafeHashMap.put(2, "Two");  
        threadSafeHashMap.put(3, "Three");  
          
        // 启动两个线程同时修改map,其中一个线程尝试更新已存在的键,另一个线程尝试删除一个键  
        new Thread(() -> {  
            threadSafeHashMap.put(4, "Four"); // 插入新键值对  
        }).start();  
        new Thread(() -> {  
            threadSafeHashMap.remove(2); // 删除键值对(2,"Two")  
        }).start();  
    }  
}
 
✔️继承关系方面
HashTable是基于陈旧的
Dictionary类继承来的。
HashMap继承的抽象类AbstractMap实现了Map接口。
ConcurrentHashMap同样继承了抽象类AbstractMap,并且实现了ConcurrentMap接口。
接下来,我们通过代码来展示它们在继承关系方面的区别:
import java.util.HashMap;  
import java.util.Hashtable;  
import java.util.concurrent.ConcurrentHashMap;  
  
public class HashMapVsHashtableVsConcurrentHashMap {  
    public static void main(String[] args) {  
        // 创建一个HashMap实例  
        HashMap<String, Integer> hashMap = new HashMap<>();  
        System.out.println("HashMap继承关系: " + hashMap.getClass().getSuperclass());  
          
        // 创建一个Hashtable实例  
        Hashtable<String, Integer> hashtable = new Hashtable<>();  
        System.out.println("Hashtable继承关系: " + hashtable.getClass().getSuperclass());  
          
        // 创建一个ConcurrentHashMap实例  
        ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();  
        System.out.println("ConcurrentHashMap继承关系: " + concurrentHashMap.getClass().getSuperclass());  
    }  
}
 
运行上面的代码,输出结果将会显示这三个类在继承关系上的不同。输出结果如下:
HashMap继承自AbstractMap。Hashtable继承自Dictionary和Hashtable。这是因为Hashtable是遗留类,设计用于Java 1.0,而Dictionary是它的超类。ConcurrentHashMap也继承自AbstractMap,与HashMap类似。这是因为它的设计目标是为了提供线程安全的哈希表,而不需要额外的线程安全机制。
✔️ 允不允许null值方面
HashTable中,key和value都不允许出现null 值,否则会抛出NullPointerException异常。
HashMap中,null 可以作为键或者值都可以。
ConcurrentHashMap中,key和value都不允许为null。
✔️为什么ConcurrentHashMap不允许null值?
我们知道,ConcurrentHashMap 在使用时,和 HashMap 有一个比较大的区别,那就是HashMap  中,null 可以作为键或者值都可以。而在 ConcurrentHashMap 中,key  和value 都不允许为null。
那么,为什么呢? 为啥ConcurrentHashMap要设计成这样的呢?
关于这个问题,其实最有发言权的就是ConcurrentHashMap的作者-Doug Lea
大家看一个截图吧。因为原文地址现在不知道怎么搞的打不开了。一张截图大家凑合看着吧。

 
主要意思就是说 :
ConcurrentMap (如 ConcurrentHashMap 、ConcurrentSkipListMap ) 不允许使用 null 值的主要原因是,在非并发的Map中(如HashMap),是可以容忍模糊性 (二义性)的,而在并发Map中是无法容忍的。
假如说,所有的 Map 都支持 null 的话,那么 map.get(key) 就可以返回 null ,但是,这时候就会存在一个不确定性,当你拿到null的时候,你是不知道他是因为本来就存了一个 null 进去还是说就是因为没找到而返回了null。
在HashMap中,因为它的设计就是给单线程用的,所以当我们map.get(key)返回nul的时候,我们是可以通过map.contains(key)检查来进行检测的,如果它返回true,则认为是存了一个null,否则就是因为没找到而返回了null。
但是,像ConcurrentHashMap,它是为并发而生的,它是要用在并发场景中的,当我们map.get(key)返回null的时候,是没办法通过map.contains(key)检查来准确的检测,因为在检测过程中可能会被其他线程所修改,而导致检测结果并不可靠。
所以,为了让 ConcurrentHashMap 的语义更加准确,不存在二义性的问题,他就不支持null。
✔️ 默认初始容量和扩容机制
HashMap的默认初始容量为16,默认的加载因子为0.75,即当HashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并将原来的元素重新分配到新的桶中。
Hashtable,默认初始容量为11,默认的加载因子为0.75,即当Hashtable中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍加1,并将原来的元素重新分配到新的桶中。
ConcurrentHashMap ,默认初始容量为16,默认的加载因子为0.75,即当ConcurrentHashMap 中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并会采用分段锁机制,将 ConcurrentHashMap 分为多个段(segment),每个段独立进行扩容操作,避免了整个ConcurrentHashMap 的锁竞争。
✔️遍历方式的内部实现上不同
HashMap 使用 EntrySet 进行遍历,即先获取到 HashMap 中所有的键值对(Entry),然后遍历Entry集合。支持 fail-fast ,也就是说在遍历过程中,若 HashMap的结构被修改(添加或删除元素),则会抛出ConcurrentModificationException,如果只需要遍历 HashMap 中的 key 或value ,可以使用KeySet或Values来遍历。
Hashtable  使用Enumeration进行遍历,即获取Hashtable中所有的key,然后遍历key集合。遍历过程中,Hashtable  的结构发生变化时,Enumeration 会失效。
ConcurrentHashMap 使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap 的某人段被修改不会影响其他段的遍历。可以使用EntrySet、KeySet或Values来遍历ConcurrentHashMap,其中EntrySet遍历时效率最高。遍历过程中,ConcurrentHashMap的结构发生变化时,不会抛出ConcurrentModificationException异常,但是在遍历时可能会出现数据不一致的情况,因为遍历器仅提供了弱一致性保障。
以下是一个8行4列的表格:
| 特性/集合类 | HashMap | Hashtable | ConcurrentHashMap | 
|---|---|---|---|
| 线程安全 | 否 | 是,基于方法锁 | 是,基于分段锁 | 
| 继承关系 | AbstractMap | Dictionary | AbstractMap,ConcurrentMap | 
| 允许null值 | K-V都允许 | K-V都不允许 | K-V都不允许 | 
| 默认初始容量 | 16 | 11 | 16 | 
| 默认加载因子 | 0.75 | 0.75 | 0.75 | 
| 扩容后容量 | 原来的两倍 | 原来的两倍+1 | 原来的两倍 | 
| 是否支持fail-fast | 支持 | 不支持 | fail-safe | 



















