在多线程环境下使用哈希表,是涉及到线程安全的,主要总结HashMap,HashTable,ConcurrentHashMap在使用时需要注意的点。
HashMap:本身是不安全的,没有一些加锁或者别的机制来保证线程安全。
在多线程环境下,要保证线程安全,可以使用HashTable和ConcurrentHashMap,这两个集合类是 线程安全的。
HashTable和ConcurrentHashMap的区别: HashTable只是把类中关键的方法加上了synchronized关键字,相当于给对象本身进行加锁,在多个线程访问同一个HashTable时就会产生锁竞争,但是使用HashTable效率很低,ConcurrentHashMap才是我们推荐使用的。
主要区别:
1.加锁粒度的不同(也就是产生锁冲突的频率)。
HashTable是针对整个哈希表加锁,任何的增删改查操作,都会加锁,也就很可能产生锁竞争。
其实我们可以考虑下,是否有必要把锁加的这么频繁,如下图: 是一张哈希表(数组+链表),插入元素时,首先要计算hash值,根据hash值找到对应的数组的下标,然后把新的元素写入链表上即可(HashMap在链表太长的时候把链表变成红黑树)。 如果两个线程插入两个元素,两个元素在不同的链表上,(两个线程修改不同的变量)此时本身就是线程安全的,那还加锁干啥呢~~
此时虽然两个线程没有线程安全问题,但是锁 是加到整个哈希表上的,此时锁对象就是this,所以仍然会对同一个对象产生锁竞争,产生阻塞等待。此时效率不就很低~
我们再来看ConcurrenthashMap是如何做的,如下图:
此时就不只是有一把锁了,把每个链表的头节点当成锁锁对象,操作不同的链表就是操作不同的锁对象,(注:两个线程针对同一个锁对象加锁才会产生锁竞争,针对不同的锁对象加锁是没有锁竞争的),这里的加锁概率就大大降低了,不产生锁竞争我就不加锁,甚至我加了锁也只是一个偏向锁,有了锁竞争,我再升级~(ConcurrenthashMap是非常来灵活的加锁,所以推荐使用此种方案解决线程安全问题)
如果说此种加锁方式还是很抽象,我们来看一段伪代码进行理解:
void put(String key,String value) {
//先找到对应链表的头节点
int index = hashCode(key); //根据hash函数计算出相应的数组下标
Node head = getHead(index);
synchronized(head) {
//执行链表插入的操作
}
}
就是类似于这种方式给每一个链表的头节点加锁的,因为每一链表的头节点也是一个对象,加锁的锁对象只是为了标识当前是不是两个线程同时对一个锁对象进行加锁,来判断是否会有锁竞争,锁对象再也没有其它的作用了。
注:上述区别是ConcurrentHashMap和HashTable最最核心的区别(重点掌握)
2.更充分利用了cas机制(无锁编程)
有些操作(比如获取 / 更新元素的个数),此时就不用再加锁来保证线程安全了,只用cas完成,如果不加锁,就不会产生锁竞争,就没有创建锁的开销,所以cas往往是比锁更高效,也能保证线程安全,那为啥不全用cas来保证线程安全呢? 这个东西不会特别经常的使用,锁是可以在各种场景下使用的(不管高效不高效),但是cas的使用场景没有锁这么多。
3.优化了扩容策略
注:HashMap元素多了会变成红黑树,但是HashTable不会,所以元素多了就要扩容。
扩容就要重新申请内存空间,然后把元素从旧的哈希表上删除,插入到新的哈希表中,如果元素很多呢,有非常多的元素,扩容再把元素搬运到新的表上,搬运一次成本是很高的,就可能导致这一次put操作就非常慢,卡顿。
ConcurrentHashMap的策略就不同了,当需要搬运的元素非常多时,并不会试图一次性的就把所有的元素搬运到新的哈希表中,而是非常快速的搬运过去一小部分,此时旧表中有元素,新表中也有元素,就相当于存在两张哈希表了,此时插入元素就往新表中插入,删除元素,就看要删除的元素在哪张表中,查找也是如此。 并且,每次操作过程中,都搬运一部分元素到新表中。