HashMap不是线程安全的,HashTable虽然是线程安全,但是该类所有的方法都用synchronized进行线程安全的控制,在高并发的情况下,同一时刻只有一个线程可以获取对象监视器,其他线程阻塞或者轮询等待,在线程竞争激烈的情况下,这种方式的效率会非常的低下。
HashTable在扩容的时候,newSize = 2 * oldSize + 1;
ConcurrentHashMap是线程安全的,使用了锁分段的思想提高了并发度。
ConcurrentHashMap为什么高效:
Hashtable低效主要是因为所有访问Hashtable的线程都争夺一把锁。如果容器有很多把锁,每一把锁控制容器中的一部分数据,那么当多个线程访问容器里的不同部分的数据时,线程之前就不会存在锁的竞争,这样就可以有效的提高并发的访问效率。这也正是ConcurrentHashMap使用的分段锁技术。将ConcurrentHashMap容器的数据分段存储,每一段数据分配一个Segment(锁),当线程占用其中一个Segment时,其他线程可正常访问其他段数据。
jdk1.7下ConcurrentHashMap的数据结构:
ConcurrentHashMap包含一个Segment数组,每个Segment包含一个HashEntry数组,当修改HashEntry数组采用开链法处理冲突,所以它的每个HashEntry元素又是链表结构的元素。
查找元素时,先通过key定位到Segment的下标位置,再找到对应的HashEntry的下标位置,然后再比较key的值。
Segment是ConcurrentHashMap的一个内部类,主要组成如下:
HashEnrty源码:
和HashMap非常类似,唯一的区别是核心的数据如value,以及链表都是volatile修饰的,保证了获取时的可见性。
原理上来说,ConcurrentHashMap采用了分段锁的技术,不会像HashTable那样不管put和get都需要同步处理,
理论上ConcurrentHashMap支持Segment数组数量大小的线程并发,当一个线程占用锁访问Segment时不会影
响到其他的Segment。
put()方法:
首先通过key定位到Segment
在具体的Segment再进行put,首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用
scanAndLockForPut()自旋获取锁:
1.尝试自旋获取锁
2.如果重试次数达到了MAX_SCAN_RETRIES则改为阻塞锁获取成功,保证能获取成功
get()方法:
get的逻辑比较简单,key通过hash之后定位到具体的Segment,
再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。