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.csdn.net/ye17186/article/details/88233505