HashMap是线程不安全的,HashTable和ConcurrentHashMap是线程安全的。
HashTable的实现线程安全的方式是:将所有的方法都加上锁,也就相当于对this加锁,此时,无论访问HashTable的任何一个元素都会加锁操作,在多线程环境下出现锁冲突的概率很大。但是我们知道,基于哈希表的结构特点,有些元素在进行并发时,不会产生线程安全问题,是不需要进行加锁控制的。
而在HashTable的基础上ConcurrentHashMap 对HashTable进行了一些优化。
优化一:ConcurrentHashMap降低了锁冲突的概率,将大锁转化成多个小锁。
我们知道,HashMap的底层是由数组来完成的,每个数组元素里面都存有一个链表。
HashTable的加锁方式相当于对整个数组进行了加锁,如下图:
假如有两个线程,对1、2处进行修改操作(增删),此时可能会产生线程安全问题,需要进行加锁来保证线程安全。
但是如果是对3、4处进行修改,他们之间不会产生线程安全问题,不需要加锁进行控制,并且整体加锁还可能会产生锁冲突,进入阻塞等待,会浪费时间。
而在ConcurrentHashMap中会让每一个链表都有各自的锁,具体来说,就是将每个链表的头结点作为锁对象,当两个线程针对同一个锁对象加锁才会出现锁竞争,而针对不同对象,虽然也会上锁,但是不会有锁竞争,如下图:
针对1、2这种情况,是针对同一个对象加锁,会产生锁冲突,会保证线程安全;
针对3、4这种情况,不是针对同一个对象加锁,也就不会产生锁冲突,减少了不必要的等待时间。
上面说的优化针对jdk1.8及其以后的版本,jdk1.8以前的版本是使用分段锁的方式,如下:
这种方式没有将锁的粒度分割得足够细,并且代码的实现也更繁琐。
优化二:ConcurrentHashMap针对读操作不加锁,只针对写操作加锁
这样优化之后,加锁情况可以被分为一下几种:
读和读,不加锁
写和写,加锁
读和写,不加锁
在很多的场景下,写和读同时进行,可能会读到一个写了一半的操作,也就是脏读。
此时ConcurrentHashMap对代码进行了加volatile和将写操作进行原子性的操作,使其在读和写的情况下,也可以保持线程安全。
优化三:ConcurrentHashMap在内部充分的使用CAS
通过使用CAS,也可以进一步减少加锁操作的数量,比如维护元素个数size等
优化四:ConcurrentHashMap针对扩容,使用了“化整为零”的方式
在HashMap/HashTable中的扩容是创建一个更大的数组将旧数组上面的元素全部转移到新数组上,并且这个操作会跟随某一次put进行,如果表中的元素个数特别多。就会出现,这一次的put比平时的put卡了很多倍,很可能会影响用户的体验。
在ConcurrentHashMap中的扩容采取部分搬运的方式,它会保留新数组和旧数组
当进行put时,会将新元素放入新数组中,并且将旧数组中的一部分元素搬运到新数组中;
在进行get时,则将新旧数组都查询;
在进行remove时,只把元素删了即可。
这样经过一段时间后,旧数组里面的元素就全部都搬运倒新数组中了,此时在释放掉旧数组。