【Java集合篇】HashMap、Hashtable 和 ConcurrentHashMap的区别

在这里插入图片描述


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中的ReentrantLockCondition 来 实现一个线程安全的、可扩展的 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());  
    }  
}

运行上面的代码,输出结果将会显示这三个类在继承关系上的不同。输出结果如下:

  1. HashMap 继承自 AbstractMap
  2. Hashtable 继承自 DictionaryHashtable。这是因为 Hashtable 是遗留类,设计用于Java 1.0,而 Dictionary 是它的超类。
  3. ConcurrentHashMap 也继承自 AbstractMap,与 HashMap 类似。这是因为它的设计目标是为了提供线程安全的哈希表,而不需要额外的线程安全机制。

✔️ 允不允许null值方面


HashTable中,keyvalue 都不允许出现null 值,否则会抛出 NullPointerException 异常。


HashMap 中,null 可以作为键或者值都可以。


ConcurrentHashMap 中,keyvalue 都不允许为null。


✔️为什么ConcurrentHashMap不允许null值?


我们知道,ConcurrentHashMap 在使用时,和 HashMap 有一个比较大的区别,那就是HashMap 中,null 可以作为键或者值都可以。而在 ConcurrentHashMap 中,key value 都不允许为null


那么,为什么呢? 为啥ConcurrentHashMap要设计成这样的呢?


关于这个问题,其实最有发言权的就是ConcurrentHashMap的作者-Doug Lea


大家看一个截图吧。因为原文地址现在不知道怎么搞的打不开了。一张截图大家凑合看着吧。


【Java集合篇】HashMap、Hashtable 和 ConcurrentHashMap的区别_第1张图片

主要意思就是说 :


ConcurrentMap (如 ConcurrentHashMapConcurrentSkipListMap ) 不允许使用 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 中的 keyvalue ,可以使用KeySetValues来遍历。


Hashtable 使用Enumeration进行遍历,即获取Hashtable中所有的key,然后遍历key集合。遍历过程中,Hashtable 的结构发生变化时,Enumeration 会失效。


ConcurrentHashMap 使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap 的某人段被修改不会影响其他段的遍历。可以使用EntrySetKeySet或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

你可能感兴趣的:(#,Java集合类,java,开发语言)