ConcurrentNavigableMap是声明了Java API提供的数据结构接口,可以在并发程序中使用它。 实现ConcurrentNavigableMap接口的类将元素存储在两个部分中:
- 唯一标识元素的键
- 声明元素的其它数据,称为值
Java API也提供了实现ConcurrentSkipListMap的类,它是通过ConcurrentNavigableMap接口的行为实现非阻塞列表的接口,在内部使用跳跃表来存储数据。跳跃表是基于并行列表的数据结构,能够获得与二叉树类似的运行效率。 查看https://en.wikipedia.org/wiki/Skip_list获取更多关于跳跃表的信息。使用跳跃表可以得到一个排序的数据结构,而不是排序列表,且能更高效的插入、检索或删除元素。
跳跃表由William Pugh在1990年引入。
当向映射表中插入元素时,使用键来对元素排序,因此所有的元素都被序列化。键需要实现Comparable接口,或者向映射表的构造函数提供Comparator类 。除了返回具体元素的方法,此类还提供了获取映射的子映射方法。
本节将学习如何使用ConcurrentSkipListMap类实现联系人映射表。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Contact的类:
public class Contact {
-
声明两个名为name和phone的私有属性:
private final String name; private final String phone;
-
实现类构造函数,初始化属性:
public Contact(String name, String phone) { this.name=name; this.phone=phone; }
-
实现返回name和phone属性值的方法:
public String getName() { return name; } public String getPhone() { return phone; } }
-
创建名为Task的类,指定其实现Runnable接口:
public class Task implements Runnable{
-
声明名为map的String和Contact类参数化的私有ConcurrentSkipListMap属性:
private final ConcurrentSkipListMap<String, Contact> map;
-
声明名为id的私有String属性,存储当前任务ID:
private final String id;
-
实现类构造函数,初始化属性:
public Task (ConcurrentSkipListMap<String, Contact> map,String id){ this.id=id; this.map=map; }
-
实现run()方法,使用任务ID和创建Contact对象的增量数在映射中存储1000个联系人。 使用put()方法存储映射表中的联系人:
@Override public void run() { for (int i=0; i<1000; i++) { Contact contact=new Contact(id, String.valueOf(i+1000)); map.put(id+contact.getPhone(), contact); } } }
-
通过创建名为Main的类,添加main()方法,实现本范例主类:
public class Main { public static void main(String[] args) {
-
创建名为map的String和Contact类参数化的私有ConcurrentSkipListMap属性:
ConcurrentSkipListMap<String, Contact> map = new ConcurrentSkipListMap<>();
-
创建包含26个Thread对象的数组,存储所有将要执行的Task对象:
Thread threads[]=new Thread[26]; int counter=0;
-
创建并启动26个任务对象,并为每个任务ID分配一个大写字母:
for (char i='A'; i<='Z'; i++) { Task task=new Task(map, String.valueOf(i)); threads[counter]=new Thread(task); threads[counter].start(); counter++; }
-
使用join()方法等待线程结束:
for (Thread thread : threads){ try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
使用firstEntry()方法得到映射表首个条目,输出数据到控制台:
System.out.printf("Main: Size of the map: %d\n",map.size()); Map.Entry<String, Contact> element; Contact contact; element=map.firstEntry(); contact=element.getValue(); System.out.printf("Main: First Entry: %s: %s\n", contact.getName(), contact.getPhone());
-
使用lastEntry()方法得到映射表最后一个条目,输出数据到控制台:
element=map.lastEntry(); contact=element.getValue(); System.out.printf("Main: Last Entry: %s: %s\n", contact.getName(), contact.getPhone());
-
使用subMap()方法获得映射表的子映射,输出数据到控制台:
System.out.printf("Main: Submap from A1996 to B1002: \n"); ConcurrentNavigableMap<String, Contact> submap=map.subMap("A1996","B1002"); do { element=submap.pollFirstEntry(); if (element!=null) { contact=element.getValue(); System.out.printf("%s: %s\n", contact.getName(), contact.getPhone()); } } while (element!=null); } }
工作原理
本节在可控制映射中实现Task类来存储Contact对象。每个联系人都有一个名称,这是创建其任务的ID,以及一个电话号码,这个号码在1000到2000之间。将这些值连接为联系人的键,每个Task对象创建1000个联系人,使用put()方法将这些联系人存储到可控制映射中。
如果插入一个映射中已存在键的元素,那么与此键关联的元素将被新元素取代。
Main()类的main()方法使用大写字母A到Z作为ID创建26个Task对象,然后用一些方法从映射表中获取数据。firstEntry()方法返回包含映射表首个元素的Map.Entry对象,此方法不删除映射表元素。对象包括键和元素,调用getValue()方法获得此元素,可以使用getKey()方法得到元素的键值。
lastEntry()方法返回包含映射表最后一个元素的Map.Entry对象,此时元素的键在A1996到B1002之间。使用pollFirst()方法处理subMap()方法的元素。此方法返回且删除子映射的首个Map.Entry对象。
下图显示本范例在控制台输出的执行信息:
扩展学习
ConcurrentSkipListMap类还有如下一些方法:
- headMap(K toKey):这里K是在ConcurrentSkipListMap对象的参数化中使用的键值的类。此方法返回映射表首个元素的子映射,其中元素比作为参数传递的元素键值小。
- tailMap(K fromKey):这里K是在ConcurrentSkipListMap对象的参数化中使用的键值的类。此方法返回映射表最后一个元素的子映射,其中元素比作为参数传递的元素键值大。
- putIfAbsent(K key, V Value):此方法插入作为参数指定的值,如果映射中不存在则将其作为参数指定的键。
- pollLastEntry():此方法返回且删除包含映射表最后一个元素的Map.Entry对象。
- replace(K key, V Value):如果映射表中存在此键,此方法则替换与指定为参数的键相关联的值。
更多关注
- 本章“使用非阻塞线程安全双端队列”小节