HashSet的底层就是HashMap
HashMap为什么线程不安全
HashMap在修改中是有fast-fail,那为啥还是不安全呢。
因为它的扩容resize操作。具体方法是,创建一个新的,长度为原来Capacity两倍的数组,保证新的Capacity仍为2的N次方,从而保证上述寻址方式仍适用。同时需要通过如下transfer方法将原来的所有数据全部重新插入(rehash)到新的数组中。
transfer方法并不保证线程安全,而且在多线程并发调用时,可能出现死循环。其执行过程如下。从步骤2可见,转移时链表顺序反转。
- 遍历原数组中的元素
- 对链表上的每一个节点遍历:用next取得要转移那个元素的下一个,将e转移到新数组的头部,使用头插法插入节点(这一步可能产生死循环)
- 循环2,直到链表节点全部转移
- 循环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为什么能保证线程安全
原因如下:
- 在put和扩容时使用了synchronized关键字、分段锁
- 大量使用乐观锁CAS以及volatile的技术
在put时,可能存在多种情况,比如map还未进行初始化(这里指没有给容器大小等操作)、该值已存放等多种情况。各种情况所用技术不同。
对于put操作:
- 如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。
- 如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。
对于get操作:
- 数组被volatile关键字修饰,因此不用担心数组的可见性问题。
- 同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。
- 而其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),以及速度。
- 继承的父类不同,HashMap继承自AbstractMap类,Hashtable继承自Dictionary类,但二者都实现了Map接口。
- HashMap线程不安全,HashTable线程安全
- Hashmap是允许key和value为null值的;HashTable键值对都不能为空,否则包空指针异常。
- 两者计算hash的方法不同:Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模;HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸
- 扩容方式不同:HashMap扩容时是当前容量翻倍即:capacity 2,Hashtable扩容时是容量翻倍+1即:capacity2+1。
- 初始容量不同:HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
- 迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。
- 由于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.youkuaiyun.com/ye17186/article/details/88233505
Redis的Hash和java的HashMap有啥区别
- HashMap是单机的,Redis的Hash是分布式的。
- HashMap是线程不安全的,Redis的Hash是线程安全的。
- 它们的扩容机制不一样,HashMap是一次性复制,RedisHash采用渐进式迁移。
下面详情说明一下他们的扩容机制:
- HashMap有一个初始容量(默认为16)和负载因子(默认为0.75)。当 HashMap 中的元素数量超过容量乘以负载因子时,就会触发扩容操作。扩容时,HashMap 的容量会增加到原来的两倍,并重新计算所有元素的哈希值,将它们重新分配到新的桶中(rehashing)。这个过程涉及到遍历整个表,因此是一个相对耗时的操作。
- Redis Hash 采用渐进式 Rehash:当 Hash 达到一定大小(例如超过 512 个元素)时,Redis 会开始将旧的哈希表中的数据逐步迁移到新的更大的哈希表中。这个迁移过程不会一次性完成,而是在每次执行命令时迁移一部分数据,直到所有数据都迁移完毕。这样可以避免因一次性大规模迁移而导致的阻塞问题。在渐进式 Rehash 期间,Redis 同时维护两个哈希表:旧表和新表。读写操作会同时在这两个表中进行,确保即使在 Rehash 过程中也不会影响服务的可用性。一旦所有的数据都迁移到新表,旧表会被释放,Rehash 结束。Redis 的 Hash 并没有固定的负载因子或容量限制,而是根据实际需求动态调整大小。这使得 Redis 的 Hash 更加灵活,适用于各种不同规模的数据集。
- ConCurrentHashMap采用渐进式迁移:不像 HashMap 那样一次性迁移所有数据,ConcurrentHashMap 采用渐进式的方式,在每次执行写操作时逐步迁移部分数据到新表中。转发节点:为了指示正在迁移中的状态,会在旧表中放置一个特殊的 ForwardingNode 节点,指向新表。这样可以确保读操作能够正确找到数据,即使它们已经被迁移到新表中。
ConcurrentHashMap 为什么不允许插入null
因为如果允许为null,就有一个二义性的问题:
- 有key,这个key的value是null
- 没有key,获取不到
在并发环境下,就会有歧义,在并发环境下,需要保证语义的严格准确,而 ConcurrentHashMap 是设计给并发环境的。
这是因为 HashMap 的设计是给单线程使用的,所以如果查询到了 null 值,我们可以通过 hashMap.containsKey(key) 的方法来区分这个 null 值到底是存入的 null?还是压根不存在的 null?
而 ConcurrentHashMap 就不一样了,因为 ConcurrentHashMap 使用的场景是多线程,所以它的情况更加复杂。 我们假设 ConcurrentHashMap 可以存入 null 值,有这样一个场景,现在有一个线程 A 调用了 concurrentHashMap.containsKey(key),我们期望返回的结果是 false,但在我们调用 concurrentHashMap.containsKey(key) 之后,未返回结果之前,线程 B 又调用了 concurrentHashMap.put(key,null) 存入了 null 值,那么线程 A 最终返回的结果就是 true 了,这个结果和我们之前预想的 false 完全不一样。