Hashtable和HashMap的区别
命名
Hashtable命名没有遵循驼峰命名,HashMap命名遵循了驼峰命名。
继承类
Hashtable继承的是Dictionary类,HashMap继承的是AbstractMap类。
线程
Hashtable在多线程的情况下是线程安全的,因为Hashtable在数据操作的方法上都加上了synchronized关键字。
//Hashtable的put方法
public synchronized V put(K key, V value)
HashMap没有实现线程安全,在多线程的状态下可能会造成环形链表和轮训。
初始化和扩容
Hashtable的默认初始化容量为11,默认加载因子是0.75f,扩容是基础容量翻一倍+1。
public Hashtable() {
this(11, 0.75f);
}
HashMap的默认初始化容量为16,默认加载因子是0.75f,扩容是基础容量翻一倍。
失败机制
Hashtable是安全失败机制(fail-safe),HashMap是快速失败机制(fail-fast)。
快速失败机制
在map使用迭代器进行遍历时,对元素进行删除,添加的操作时,会抛出java.util.ConcurrentModificationException异常。
java.util包下的集合类都是快速失败的。
HashMap<Integer,Integer> map = new HashMap();
map.put(1,1);
map.put(2,2);
map.put(3,3);
map.put(4,4);
map.put(5,5);
map.put(6,6);
Set<Integer> set = map.keySet();
Iterator<Integer> it = set.iterator();
while(it.hasNext()){
Integer key = it.next();
map.put(key+10,22);
}
//Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
at java.util.HashMap$KeyIterator.next(HashMap.java:1466)
at RBTree.Test.main(Test.java:29)
安全失败机制
java.util.concurrent包下的集合类都是安全失败的
线程A正在遍历Hashtable,线程B此时进行修改,删除,添加的操作,线程A是拿不到最新的数据的,因为采用安全失败机制的容器先复制原有集合内容,在拷贝的集合上进行遍历。
HashMap在JDK1.7和1.8的区别
底层结构
1.7数组+链表
1.8数组+链表+红黑树。考虑到在数据量大的情况下导致某一桶的链表过长,使查询速度变慢的情况,加入的红黑树。当桶的长度大于64且链表长度大于8的时候,就会将链表转化为红黑树,如果链表长度小于等于6,红黑树又会退化为链表。
插入法
1.7头插法,可能会形成环,造成死循环。
1.8尾插法
扩容
1.7会颠倒链表的顺序;1.7则是在元素插入前。
1.8会保持原链表的顺序,而且1.8是在元素插入后检测是否需要扩容。
ConcurrentHashMap
实现线程安全的原理
JDK1.7 segment+ReentrantLock+HashEntry
通过将一个整体的桶分成一个个segment,桶和entry里的key,value都实现了volatile关键字,volatile关键字可以保证数据的有序性和可见性,也就是一个线程对数据进行了修改,其他线程也能立刻看到最新的数据。所以ConcurrentHashMap在多线程情况下get()方法不需要添加synchronized关键字也能保证线程安全,并且效率高。
在进行put()时,当前线程占据当前segment的锁,其余的线程会进行自旋,期间如果线程空出,那就会抢锁。当自旋达到一定次数依旧没有获得锁,就会进入阻塞队列。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
JDK1.8 CAS+synchronized+node
在JDK1.8中,弃用了segment,将hashEntry换成了node,好处是降低了锁的粒度,减少了并发冲突。原来需要锁整块segment,现在只需要锁头部node。在put()时,判断节点为null就利用CAS插入,失败就自旋保证成功。在桶长度>64且链表长度大于8时同样会转化为红黑树。
CAS
t1线程拿到执行权限,t2线程被告知失败。t1和t2都各自存有int v = 20这个共享变量的副本V1,V2,t1线程会把副本值V1和预期值E1比较,如果V1=E1,则更新变量V1=21,并将V1写入主内存,v = 21。此时t2抢到执行权限,比较V2和E2,发现不相等,更新失败,重新更新副本,最后写入成功。
其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg
指令,也就是说CAS是靠硬件实现的,从而在硬件层面提升效率。