HashMap、Hashtable、ConcurrentHashMap的区别以及为什么线程安全

HashSet的底层就是HashMap

HashMap为什么线程不安全

HashMap在修改中是有fast-fail,那为啥还是不安全呢。

因为它的扩容resize操作。具体方法是,创建一个新的,长度为原来Capacity两倍的数组,保证新的Capacity仍为2的N次方,从而保证上述寻址方式仍适用。同时需要通过如下transfer方法将原来的所有数据全部重新插入(rehash)到新的数组中。

transfer方法并不保证线程安全,而且在多线程并发调用时,可能出现死循环。其执行过程如下。从步骤2可见,转移时链表顺序反转。

  1. 遍历原数组中的元素
  2. 对链表上的每一个节点遍历:用next取得要转移那个元素的下一个,将e转移到新数组的头部,使用头插法插入节点(这一步可能产生死循环)
  3. 循环2,直到链表节点全部转移
  4. 循环1,直到所有元素全部转移

大致语句如下:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {e.hash = null == e.key ? 0 :hash(e.key);}
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

单线程插入如下:
在这里插入图片描述
多线程插入如下:

这里假设有两个线程同时执行了put操作并引发了rehash,执行了transfer方法,并假设线程一进入transfer方法并执行完next = e.next后,因为线程调度所分配时间片用完而“暂停”,此时线程二完成了transfer方法的执行。此时线程一、线程二的状态分别如下。
在这里插入图片描述
接着线程1被唤醒,继续执行第一轮循环的剩余部分,我们会发现它最终执行结果如下,产生了死循环。线程一接下来要执行的是,将key为9的entry插入进去,再将key9的下一个插入进去,但因为线程二头插法,已经将key5插入进去,成为key9的下一个,所以就会形成循环。
在这里插入图片描述

ConcurrentHashMap为什么能保证线程安全

原因如下:

  1. 在put和扩容时使用了synchronized关键字、分段锁
  2. 大量使用乐观锁CAS以及volatile的技术

在put时,可能存在多种情况,比如map还未进行初始化(这里指没有给容器大小等操作)、该值已存放等多种情况。各种情况所用技术不同。

对于put操作:

  1. 如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。
  2. 如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。

对于get操作:

  1. 数组被volatile关键字修饰,因此不用担心数组的可见性问题。
  2. 同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。
  3. 而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。

synchronized关键字、分段加锁
比如在put方法中,在map已经初始化,且该值已经存放过,我们是对原值进行修改时,加锁进行put,其中f变量只是一个node节点。
源码如下:

else {
         V oldVal = null;
         synchronized (f) {……}
         ……
     }

乐观锁CAS、volatile
在一种情况下,使用CAS、volatile,具体使用compareAndSwapObject,这种底层的原子操作。而其中的tab是属于table的,table是一个volatile的变量。

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

这里调用了casTabAt方法,其中调用了compareAndSwapObject,这是一个Unsafe类的原子操作。

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

jdk1.8中,对hashMap和concurrentHashMap做了哪些优化

对于hashMap的优化

jdk1.8之前,其数据结构为数组加链表。jdk1.8之后的优化,其数据结构变成了数组+链表+红黑树

当链表上的结点过多时,查询一个结点,在jdk1.8之前,需要遍历整个节点,效率为O(n)。而在jdk1.8中,如果结点达到阈值TREEIFY_THRESHOLD(默认为8)时,会将链表结构转成红黑树结构,这样再查询时,如果数组的first结点是树结构,则采用树的查询算法,效率为O(logn),否则还是遍历链表。

当树上结点过多时,阈值为UNTREEIFY_THRESHOLD(默认为6),会进行树转链表操作。

至于为什么不是8,是为了防止频繁的进行树–链表的转换。

对于concurrentHashMap的优化

jdk1.8之前,ConcurrentHashMap通过将整个Map划分成N(默认16个)个Segment,而Segment继承自ReentrantLock ,通过对每个Segment加锁来实现线程安全。而在jdk1.8后,摒弃了这种实现方式,采用了CAS + Synchronized,对链表头结点进行加锁,来实现线程安全。参考jdk1.8源码

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

  1. 继承的父类不同,HashMap继承自AbstractMap类,Hashtable继承自Dictionary类,但二者都实现了Map接口。
  2. HashMap线程不安全,HashTable线程安全
  3. Hashmap是允许key和value为null值的;HashTable键值对都不能为空,否则包空指针异常。
  4. 两者计算hash的方法不同:Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模;HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸
  5. 扩容方式不同:HashMap扩容时是当前容量翻倍即:capacity 2,Hashtable扩容时是容量翻倍+1即:capacity2+1。
  6. 初始容量不同:HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
  7. 迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。
  8. 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。

Hashtable
HashMap是非synchronized,而Hashtable是synchronized,这意味Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

HashMap不能保证随着时间的推移Map中的元素次序是不变的。

用法

Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。

ConcurrentHashMap

ConcurrentHashMap自然是线程安全的,ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁(CAS算法,乐观锁)。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);

原文链接:https://blog.csdn.net/ye17186/article/details/88233505

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值