HashTable原理:
Hashtable相当于线程安全的hashmap,其用synchronized保证线程安全性。在hashtable中,put和remove都是同步方法,在put时就不能进行remove操作。因此在线程竞争激烈的情况下,其效率非常低下。ConcurrentHashmap便对此进行了优化。
ConcurrentHashMap原理:
ConcurrentHashMap使用了锁分段技术,即首先将数据分成一段一段的存储,然后每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组和HashEntry数据结构组成,segment是一种可重入锁ReentranLock,HashEntry用于存储键值对,一个ConcurrentHashMap里包含一个Segment数组,segment结构与hashmap结构类似,是一种数组和链表结构,一个segment里包含一个hashentry数组,每个hashentry是一个链表结构的元素,每个segment守护着一个hashentry数组里的元素,当对hashentry元素进行修改时,必须先获得他所对应的segment锁。
结构如下:
两者put比较:
HashTable的put方法:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
从代码中可以看出HashTable的put方法都用了synchronized关键字实现同步,并且key值不为空。这样相当于每次进行put 的时候都会进行同步,当10个线程同步进行操作的时候,就会发现当第一个线程进去,其他线程必须等待第一个线程执行完成,才可以进行下去。性能特别差。
ConcurrentHashMap的put方法如下:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);**//根据散列函数,计算出key值的散列值**
int j = (hash >>> segmentShift) & segmentMask;//定位Segment的数组下标
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
segment的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;//Segment对应的HashEntry数组长度
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);//获取HashEntry数组的第一个值
for (HashEntry<K,V> e = first;;) {
if (e != null) {//HashEntry数组已经存在值
K k;
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { //key值和hash值都相等,则直接替换旧值
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;//不是同一个值则继续遍历,直到找到相等的key值或者为null的HashEntry数组元素
}
else {//HashEntry数组中的某个位置元素为null
if (node != null)
node.setNext(first);//将新加入节点(key)的next引用指向HashEntry数组第一个元素
else//已经获取到了Segment锁
node = new HashEntry<K,V>(hash, key, value, first)
int c = count + 1;
if (c > threshold && tab.lenth < MAXIUM_CAPACITY)//插入前先判断是否扩容,ConcurrentHashMap扩容与HashMap不同,ConcurrentHashMap只扩Segment的容量,HashMap则是整个扩容
rehash(node);
else
setEntryAt(tab, index, node);//设置为头节点
++modCount;//总容量
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
上述代码可看出,segment里面才是真正的hashtable,这里需要找出entry在table中的那个位置,之后得到的entry就是这个链中的第一个节点,如果e!=null,说明找到了,这时就要替换节点中的值,否则我们需要new一个node,将新的entry插到链表头节点。
put方法首先定位到segment,然后在segment里面进行插入操作。插入操作需要两个步骤,第一判断是否需要对segment里面的hashentry数组进行扩容,第二步找到元素添加的位置然后放到hashentry数组中。