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

2024-01-10 02:13:12

在这里插入图片描述



?? 三者区别


?? 线程安全方面


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


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


在这里插入图片描述

主要意思就是说 :


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列的表格:

特性/集合类HashMapHashtableConcurrentHashMap
线程安全是,基于方法锁是,基于分段锁
继承关系AbstractMapDictionaryAbstractMap,ConcurrentMap
允许null值K-V都允许K-V都不允许K-V都不允许
默认初始容量161116
默认加载因子0.750.750.75
扩容后容量原来的两倍原来的两倍+1原来的两倍
是否支持fail-fast支持不支持fail-safe

文章来源:https://blog.csdn.net/Java_Yangxiaoyuan/article/details/135415478
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。