HashMap: 线程不安全. key 允许为 null。
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.。只是简单的把关键方法上加上了 synchronized 关键字。如 get 和 set ,这相当于直接针对 Hashtable 对象本身加锁,如果多线程访问同一个 Hashtable 就会直接造成锁冲突.。size 属性也是通过 synchronized 来控制同步, 也是比较慢的。一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低。当多个线程进行一些更复杂的操作,比如判定get 的值就是xxx,再进行put 就会产生线程安全问题。例如
Hashtablehash = new Hashtable<>(); if(hash.get(1) == null){ hash.put(1, 2); }
虽然get是加锁的,但这里的 if 和 put 不是原子的,当多个线程线程同时进行这个操作的话,就会导致前一个线程判断为空,另一个线程也判断为空,这就导致这个操作进行了多次,但实际上只需要进行一次,这就出现了线程安全问题。
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null。相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例:
1)读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁.
2)加锁的方式仍然是是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 减小了锁的粒度,大大降低了锁冲突的概率.。
3)充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.。
4)优化了扩容方式: 化整为零 ,发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去,扩容期间, 新老数组同时存在。后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程, 每个操作负责搬运一小部分元素,搬完最后一个元素再把老数组删掉,这个期间, 插入只往新数组加, 查找需要同时查新数组和老数组。
ConcurrentHashMap每个哈希桶都有一把锁,只有两个线程同时访问同一个哈希桶才会发生锁冲突。