1、底层数据结构是什么?
在研究ConcurrentHashMap的底层数据结构之前,我们先要明确ConcurrentHashMap类中的属性,其中,有两个最重要的属性,分别是HashEntry和Segment。
HashEntry是ConcurrentHashMap最基本的储存数据的单位,主要存储键值对,其源码是:
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
可以看出是一个链表型数据结构。
而Segment是ConcurrentHashMap的一个内部类,继承自ReentrantLock类,实现了Serializable接口。
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
ReentrantLock是一种重入锁,说明Segment自身是带有锁机制的,可以保证线程安全,并且是可序列化的(Serializable)。
并且从其构造函数可以看出,它参数传的是HashEntry类型的一个数组,而HashEntry本身是个链表,所以每个Segment对象其实都是一个Map类似的数组加链表的结构。
在ConcurrentHashMap的构造函数中,会初始化一个Segment数组出来,所以ConcurrentHashMap类的底层数据结构模式应该如下图所示:
2、通过什么保证线程安全?
刚刚已经提到过,ConcurrentHashMap中有着Segment这个属性。而Segment继承自ReentrantLock,所以每一个Segment都是通过重入锁来保证Segment内部以HashEntry为基本单位的Map的线程安全。
ConcurrentHashMap是通过Segment下的put()方法存入数据的,在每一次进行put()操作时,都会加一次锁:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
这样就能保证在多线程并发执行存入数据时的线程安全性。
3、HashTable和ConcurrentHashMap线程安全保证机制是否一样?
HashTable的线程安全保证机制是通过在方法前用synchronized关键字修饰,通过对当前对象加锁保证线程安全。但是当一个线程访问同步方法时,其他线程也访问同步方法时,可能会进入阻塞或轮询状态,这样就会将效率大大降低。
而ConcurrentHashMap是通过创建Segment数组(默认大小为16),以分段式的加锁方式来保证线程安全,每一把锁只锁其中的一部分数据,多线程访问不同数据段的数据,就不会存在锁竞争,这样在一个线程进行操作时,其他线程进行访问方法的时候也可以进行操作。
而且ConcurrentHashMap中的get()方法并没有加锁,这样就可以实现多线程同时读取,由此可见,ConcurrentHashMap的效率比HashTable的效率更高。
4、HashMap、HashTable和ConcurrentHashMap区别是什么?
HashMap与ConcurrentHashMap:
首先,HashMap是没有锁机制的,所以HashMap不是线程安全的。其次HashMap的键值对是可以存储null值的,而我们通过ConcurrentHashMap源码的分析可以看出:
ConcurrentHashMap对整个Segment数组进行分割,分割后的每一段都通过lock进行加锁,保证了线程安全。
HashEntry<K,V> node = tryLock() ? null :
而且ConcurrentHashMap中键值对都是不允许含有null值的。
HashTable与ConcurrentHashMap:
之前提到过,HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,一次锁住整个hash表,从而同一时刻只能有一个线程对其进行操作。当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。
个人觉得ConcurrentHashMap就相当于一个HashTable的集合,先是Segment [ ] (默认大小为16)中每一个Segment下面都是独立的lock,一次只锁住一个map,这样就能保证在多个线程在访问时的安全性。能够同时最少16个线程进行访问(读取时没有加锁,可保证多个线程对同一目标进行读取)。