Java集合类有的集合类是存在线程安全的问题,但是由于之前对于集合类的使用都是在单线程的情况下使用的,不没有在多线程环境下使用,所以不涉及线程安全的问题;这篇博客着重讲解一下多线程环境下使用哈希表。
HashMap
HashMap本身不是线程安全的,所以在单线程的情况下可以放心使用,在多线程的情况下谨慎使用(可能会触发线程安全问题)。
HashTable
HashTap锁机制(HashTable如何实现加锁的)
HashTable相比于HashMap是线程安全的,可以在多线程的情况下使用;
1.源码分析
HashTable把哈希表中的关键方法(put,get等)进行了加锁操作:
通过源码可以看出,HashTable底层是通过synchronized对方法加了锁;
2.画图分析
这个图大致反映了底层如何对哈希表进行加锁的:对整个哈希表加了锁,此时只要是多个线程访问同一个HashTable对象时就会造成锁冲突!但是这种方法效率较为低,当多个线程访问不同链表上的数据时并不存在线程安全问题,此时加锁进行阻塞等待显得没有必要。
ConcurrentHashMap
Java1.8下的ConcurrentHashMap
Java1.8中,synchronized锁把每个链表的头结点作为锁对象,此时只有当多个线程访问并操作同一个链表下的数据时就会发生锁冲突,若访问的不是同一个链表下的数据时并不会发生锁冲突。
Java1.7和之前的ConcurrentHashMap
在Java1.7和之前,synchronized锁实现的是分段加锁,每三个链表加一次锁,本质上缩小了锁的范围,但是粒度切分的不够细,而且代码的实现较为繁琐。
ConcurrentHashMap相对于HashTable的优化
ConcurrentHashMap相比于HashTable大大缩小了锁的冲突概率
ConcurrentHashMap把HashTable中的对整个哈希表加锁的操作拆分成了很多把小锁,HashTable中只要操作哈希表上的任意元素都会发生锁冲突,而ConcurrentHashMap只有多个线程操作同一个链表上的元素时才会存在线程安全问题(此时加锁控制线程安全),操作不同链表上的数据时并不会发生锁冲突(这也大大提高了效率),即只有多个对象针对同一个对象加锁时才会有锁竞争,才会有阻塞等待,针对不同对象,没有锁竞争!
ConcurrentHashMap做了一个激进的操作,针对读操作不加锁,只针对写操作加锁
我们知道,在线程安全问题中:
读和读之间,没有冲突
写和写之间,有冲突
读和写之间,有冲突(因为存在脏读情况,可能会读到一个写了一半的结果)
所以在ConcurrentHashMap中,用volatile关键字+原子的写操作来避免这种脏读情况,从而控制了线程安全。
ConcurrentHashMap内部充分使用了CAS
CAS操作在CPU上一条原子性的指令,CAS避免使用锁而达到和锁一样控制线程安全的效果(只是CAS不会阻塞线程,多个线程对某个资源进行CAS操作时,只能有一个线程会操作成功,不成功的线程并不会被阻塞,只是会收到操作失败的信号),所以ConcurrentHashMap内部充分使用CAS就是为了在一定程度上减少锁的数目,从而减少开销来提高效率;另一方面size属性也可以通过CAS操作来进行更新,避免了重量级锁的情况。
ConcurrentHashMap针对扩容,采取了“化整为零”的方式
HashMap/HashTable扩容:
创建一个更大的数组空间,把旧的数组上的链表上的每一个元素搬运到新的数组上,如果元素个数特别多的时,这样的搬运操作就会非常耗时;
ConcurrentHashMap扩容:
每次只会搬运一小部分的元素,旧的数组也会保留,每次put操作都往新数组上添加元素,同时进行一部分搬运(把一小部分旧的元素搬运到新数组上),每次get的时候,旧数组和新数组都会进行查询,每次remove的时候就把元素删除,经过一定的时间后,所有的元素都搬运好了,最后再释放旧数组。ConcurrentHashMap可以看成是在HashTable的基础上“化整为零”。
常见面试题
ConcurrentHashMap的读是否要加锁,为什么?
答:读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了
volatile 关键字.
介绍下 ConcurrentHashMap的锁分段技术?
答:这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个
"段" (Segment), 针对每个段分别加锁. 目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
ConcurrentHashMap在jdk1.8做了哪些优化?
答:取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对
象). 将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于
8 个元素)就转换成红黑树.
Hashtable和HashMap、ConcurrentHashMap 之间的区别?
答:HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
CAS 机制. 优化了扩容方式. key 不允许为 null