哈希表是将键映射到值的数据结构,通常在内部使用数组存储元素和使用键计算元素在数组中位置的哈希函数。这种数据结构的主要优点是插入、删除和检索操作速度非常快,所以在执行大量检索时非常有用。
Java API通过Map和ConcurrentMap接口提供了不同的哈希表实现。ConcurrentMap接口为所有操作提供线程安全和原子保证,因此可以在并发应用中使用。 ConcurrentHashMap类实现ConcurrentMap接口,并向接口中声明的方法添加了更多的方法。此类支持如下特性:
- 读操作的完全并发性
- 插入和删除操作的高预期并发性
Java 版本5中引入这两类元素(类和接口),但在版本8中,提供了许多与流API提供的方法类似的新方法。
本节学习在应用中如何使用ConcurrentHashMap类及其提供的重要方法。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Operation的类,包括三个属性:名为user的String属性,名为operation的String属性,和名为time的Date属性。添加这些属性的读取值方法。代码很简单,不在这里列出。
-
创建名为HashFiller的类,指定其实现Runnable接口:
public class HashFiller implements Runnable{
-
声明名为userHash的私有ConsurrentHashMap属性,哈希的键是String类型,值是Operation对象的ConcurrentLinkedDeque对象。实现类构造函数,初始化属性:
private ConcurrentHashMap<String, ConcurrentLinkedDeque<Operation>> userHash; public HashFiller(ConcurrentHashMap<String, ConcurrentLinkedDeque <Operation>> userHash) { this.userHash = userHash; }
-
实现run()方法,将100个随机Operation对象填充到ConsurrentHashMap。首先,生成随机数据,然后在哈希中使用addOperationToHash()方法插入对象:
@Override public void run() { Random randomGenerator = new Random(); for (int i = 0; i < 100; i++) { Operation operation = new Operation(); String user = "USER" + randomGenerator.nextInt(100); operation.setUser(user); String action = "OP" + randomGenerator.nextInt(10); operation.setOperation(action); operation.setTime(new Date()); addOperationToHash(userHash, operation); } }
-
实现addOperationToHash()方法,接收作为参数添加的散列和操作,映射中的键将是分配操作的用户。使用computeIfAbsent()方法来获取与键相关联的ConcurrentLinkedDeque对象。如果键存在,此方法返回与键相关联的值。如果不存在,则执行作为参数传递给此方法的lambda表达式,以生成值并与键关联。 在这里,我们生成新的ConcurrentLinkedDeque对象。最后,将此操作插入到双端队列中:
private void addOperationToHash(ConcurrentHashMap<String, ConcurrentLinkedDeque<Operation>> userHash2, Operation operation) { ConcurrentLinkedDeque<Operation> opList = userHash.computeIfAbsent(operation.getUser(), user -> new ConcurrentLinkedDeque<>()); opList.add(operation); } }
-
现在实现包含main()方法的Main类。首先,声明ConcurrentHashMap对象和HashFiller任务:
ConcurrentHashMap<String, ConcurrentLinkedDeque<Operation>> userHash = new ConcurrentHashMap<>(); HashFiller hashFiller = new HashFiller(userHash);
-
使用HashFiller类执行10个线程,然后使用join()方法等待线程结束:
Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(hashFiller); threads[i].start(); } for (int i = 0; i < 10; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
现在,提取ConcurrentHashMap信息。首先,使用size()方法提取存储在映射表中的元素数量。然后,使用forEach()方法对存储在哈希表中的所有元素引入一个操作。第一个参数是并行阈值,是能够以并发方式执行操作所需的最小元素数量。我们指定此参数值为10,并且哈希表有100个元素,所以能够并行执行操作。lambda表达式接收两个参数:键和值。输出键和ConcurrentLinkedDeque存储的长度作为值到控制台:
System.out.printf("Size: %d\n", userHash.size()); userHash.forEach(10, (user, list) -> { System.out.printf("%s: %s: %d\n", Thread.currentThread().getName(), user, list.size()); });
-
然后使用forEachEntry()方法,与forEach()相似,但lambda表达式接收Entry对象作为参数。可以使用这个entry对象来获得键和值:
userHash.forEachEntry(10, entry -> { System.out.printf("%s: %s: %d\n", Thread.currentThread().getName(), entry.getKey(), entry.getValue().size()); });
-
然后,使用search()方法查找满足指定检索函数的第一个元素。 本范例中,检索操作代码以1结尾的操作。对于forEach()方法,我们指定了并行阈值:
Operation op = userHash.search(10, (user, list) -> { for (Operation operation : list) { if (operation.getOperation().endsWith("1")) { return operation; } } return null; }); System.out.printf("The operation we have found is: %s, %s, %s,\n", op.getUser(), op.getOperation(), op.getTime());
-
再次使用search()方法,但这次用来查找有10个以上操作的用户:
ConcurrentLinkedDeque<Operation> operations = userHash.search(10, (user, list) -> { if (list.size() > 10) { return list; } return null; }); System.out.printf("The user we have found is: %s: %d operations\n", operations.getFirst().getUser(), operations.size());
-
最后,使用reduce()方法计算存储在哈希表中的操作总数:
int totalSize = userHash.reduce(10, (user, list) -> { return list.size(); }, (n1, n2) -> { return n1 + n2; }); System.out.printf("The total size is: %d\n", totalSize);
工作原理
本节中,实现了使用ConcurrentHashMap来存储用户操作的信息的范例。哈希表在内部使用Operation类的user属性作为键,以及ConcurrentLinkedDeque(非阻塞并发列表)作为值来存储与此用户关联的所有操作。
首先,使用10个不同的线程填充一些随机数到哈希表中,为此实现了HashFillter任务。这些任务最大的问题是在哈希表中需要插入键时出现的情况。如果两个线程在同一时刻添加相同的键时,其中一个线程插入的数据将会丢失并具有数据竞争条件。为了解决这个问题,我们用到computeIfAbsent()方法。
此方法接收键和表示为lambda表达式的函数接口实现,将键和接口实现作为参数接收。如果键存在,此方法返回与键相关联的值。如果不存在,此方法执行指定的Function对象,并将函数返回的键和值添加HashMap中。本范例中,键是不存在的,所以我们创建了ConcurrentLinkedDeque类的新实例。此方法的主要优点是能够自动执行,也就是说,如果其它线程尝试执行相同操作,它将被阻塞,直到此操作完成。
然后,在main()方法中,用到ConcurrentHashMap的其它方法来处理存储在哈希表中的信息,方法如下:
- forEach():此方法将BiConsumer接口实现作为参数接收,此接口可以表示为lambda表达式。 lambda表达式的其它两个参数分别代表键和正在处理的元素值。此方法将表达式应用于存储在ConcurrentHashMap中的所有元素。
- forEachEntry():此方法与上一个方法相同,但是这里的表达式是Consumer接口实现。它将Entry对象作为参数接收,此对象存储键和正在处理的条目值。这也是表达相同功能的另一种方式。
- search():此方法将BiFunction接口实现作为参数接收,此接口可以表示为lambda表达式。此函数还接收作为参数处理的ConcurrentHashMap对象条目的键和值,返回BiFunction返回的第一个非空值。
- reduce():此方法接收两个BiFunction接口,将ConcurrentHashMap的元素减少到一个唯一值,可以使用ConcurrentHashMap的元素实现MapReduce操作。第一个BiFunction接口将元素的键和值转换成唯一值,第二个BiFunction接口聚合两个不同元素的值。
到目前为止所描述的所有方法都有一个名为parallelismThreshold的第一个参数 。此参数被描述为"…并行执行此操作所需的(预估的)元素数量… ",也就是说,如果ConcurrentHashMap的元素比参数中指定的值少,那么此方法将按顺序执行。反之(如本范例),此方法则并行执行。
扩展学习
ConcurrentHashMap具有比前一节中讲解的更多的方法,如下所示部分方法:
forEachKey()和forEachValue():这两个方法与forEach()方法相似,但这里,表达式分别处理在ConcurrentHashMap中存储的键和值。
searchEntries()、searchKeys()和searchValues():这些方法与之前介绍的search()方法相似。不过在这里,作为参数传递的表达式接收Entry对象、键或存储在ConcurrentHashMap中的元素的值。
reduceEntries()、reduceKeys()和reduceValues():这些方法与之前介绍的reduce()方法相似。不过在这里,作为参数传递的表达式接收Entry对象、键或存储在ConcurrentHashMap中的元素的值。
reduceXXXToDouble()、reduceXXXToLong()和reduceXXXToInt():这些方法分别通过生成double、long或int值来减少ConcurrentHashMap的元素。
computeIfPresent():此方法补充computeIfAbsent()方法,接收能够表示为lambda表达式的BiFunction接口的键和实现。如果键在HashMap存在,此方法应用表达式计算键的新值。BiFunction接口将键和此键的实际值作为参数接收,并返回新值。
merge():此方法接收键、值和能够表示为lambda表达式的BiFunction接口实现,它们作为参数被接收。如果键在ConcurrentHashMap中不存在,则此键插入且将参数值与之关联。如果存在,执行BiFunction计算关联键的新值。BiFunction接口将键及其实际值作为参数接收,返回与键相关联的新值。
getOrDefault():此方法将键和默认值作为参数接收。如果键在ConcurrentHashMap存在,则返回相关联的值。否则,返回默认值。
更多关注
- 本章“使用线程安全的可操纵映射”小节
- 第六章“并行和反应流”中的“归约流元素”小节