本文的读者应该是已经掌握了基本的Java多线程开发技巧,但不熟悉Java Concurrency包的程序员。本文是本系列的第六篇文章,前五篇文章请看这里:
Java Concurrency代码实例之一执行者与线程池
Java Concurrency代码实例之二并发队列
Java Concurrency代码实例之三原子变量
Java Concurrency代码实例之四-锁
Java Concurrency代码实例之五-同步工具
1. 前言
按照用途与特性,Concurrency包中包含的工具被分为六类(外加一个工具类TimeUnit),即:
1. 执行者与线程池
2. 并发队列
3. 同步工具
4. 并发集合
5. 锁
6. 原子变量
由于前五篇文章一次介绍Concurrency包中的一整类特性,导致文章冗长,重点不突出,效果反而不好。因而自此以后,一篇仅介绍一个具体类。本文介绍的是并发集合中最常用的一个类ConcurrentHashMap。
2. 三大Map的区别
谈到ConcurrentHashMap,就不能不提它的前辈HashMap和Hashtable。
HashMap是Java中最常用的一个Map类了,它性能好、速度快,但不能保证线程安全,它可以使用null作为key或者value。
Hashtable是Java中最老的Map类,自JDK1.0版本就存在了,它是一个线程安全的Map类,其公有方法均使用synchronize关键字修饰,这表示在多线程操作时,每个线程在操作之前都会锁住整个map,待操作完成后才释放,这必然导致多线程时性能不佳。另外,Hashtable不能使用null作为key或者value。
ConcurrentHashMap是HashMap的并发类,它是线程安全的。与Hashtable相比,由于它大量使用了自旋等待、多segment等并发技术,使得并发操作时往往不需要锁住整个map,因此其多线程性能远超Hashtable。它也不能使用null作为key或者value。
3. HashMap原理概述
3.1 Hash函数与结构
HashMap是一个存储键值对
3.2 公有方法
HashMap中的公有方法是很少的,主要包括:
插入:put(K key, V value)和putAll(Map<? extends K, ? extends V> m)
,分别用来插入一个键值对和一组键值对;
查询:containsKey(Object key)用来查询Map中是否包含key,containsValue(Object value)用来查询Map中是否包含value;
获取:get(Object key),用来根据key获取value;
删除:remove(Object key)和clear(),分别用来删除一个键值对和清空Map;
长度:size()和isEmpty()。
3.3 三大集合与迭代子
除了以上方法以外,HashMap使用三大集合和三种迭代子来轮询其Key、Value和Entry对象,其使用方法如下所示:
public class HashMapExam {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(16);
for (int i = 0; i < 15; i++) {
map.put(i, new String(new char[]{(char) ('A'+ i)}));
}
System.out.println("======keySet=======");
Set<Integer> set = map.keySet();
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("======values=======");
Collection<String> values = map.values();
Iterator<String> stringIterator=values.iterator();
while (stringIterator.hasNext()) {
System.out.println(stringIterator.next());
}
System.out.println("======entrySet=======");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry);
}
}
}
3.4 自动扩容
HashMap的hash表是一个原生数组Entry<K,V>[] table
,这个原生数组的长度在HashMap创建时就被确定。程序员可以在构造函数中使用initialCapacity(初始容量,默认为16)和loadFactor(负载因子,默认为0.75)来指定HashMap的容量以及自动扩容的时机。
initialCapacity用来指定初始容量,HashMap的容量都是2的幂,若initialCapacity不是正好等于2的幂,则被向上取值为最近的一个2的幂值;loadFactor负载因子用来决定扩容的门槛值threshold,这个threshold就是容量与负载因子的乘积,当Map的size值大于threshold时,就会在put时检测是否进行扩容。当满足size大于threshold,且当前插入的Key在hash表中的相应位置已经有值(即该键值对要加入一个单向链表)时,Map会自动扩容为原来的2倍。
下面的例子展示了HashMap扩容的特性:
public class HashMapExam2 {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(10);
System.out.println("map initialize");
showMap(map);
for (int i = 0; i < 20; i++) {
map.put(i, new String(new char[]{(char) ('A'+ i)}));
System.out.println("put "+i);
showMap(map);
}
}
private static void showMap(Map<Integer, String> map) {
try {
Field f = HashMap.class.getDeclaredField("table");
f.setAccessible(true);
Map.Entry<Integer,String>[] table= (Map.Entry<Integer, String>[]) f.get(map);
f = HashMap.class.getDeclaredField("threshold");
f.setAccessible(true);
int threshold = (int) f.get(map);
System.out.println("map size = "+map.size()+",threshold = "+threshold+",length="+table.length);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
例子中使用反射获取了HashMap的Map.Entry<Integer,String>[] table
和threshold变量。从运行结果可以看出,当Map为空时,table的长度为0;当put了一个元素时,table长度被初始化为16,而threshold为12;当put了17个元素时,table扩容为32,而threshold为24。
4. ConcurrentHashMap原理概述
此文章仅描述JDK1.7版本中的ConcurrentHashMap。与HashMap相比,虽然ConcurrentHashMap仅仅只增加了并发特性,但是其复杂度却极大的上升了。因为考虑到并发性能,它没有像Hashtable一样简单的给每个公有方法加上synchronize,而是利用了JUC包中提供的多种并发特性,在尽量保持性能的前提下实现了多线程安全。顺便说一句,《Java编程思想》的作者曾经提到,Hashtable类已经基本上可以废弃了,因为它能做的ConcurrentHashMap都能做,且性能更好。Hashtable类存在的意义可能就是用来面试吧。
4.1 ConcurrentHashMap与Hashtable之性能比较
为了验证以上的说法,写了一段代码来测试ConcurrentHashMap与Hashtable之性能比较:
public class ConcurrentHashMapVsHashtable {
private static int INPUT_NUMBER = 100000;
public static void main(String[] args) throws InterruptedException {
// Map<Integer, String> map = new Hashtable<>(12 * INPUT_NUMBER);
Map<Integer, String> map = new ConcurrentHashMap<>(12 * INPUT_NUMBER);
long begin = System.currentTimeMillis();
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
service.execute(