并行流处理海量数据集与并发数据结构
在处理海量数据集时,并行流和并发数据结构是非常重要的工具。下面将介绍如何使用并行流处理海量数据集,以及Java并发API提供的并发数据结构和同步机制。
并行流处理海量数据集
我们将使用一个社交网络中计算用户间共同联系人的示例来讲解并行流的使用。
并发版本
首先,我们分析该算法的并发版本。
-
CommonPersonMapper类
:这是一个辅助类,用于从
Person
对象生成所有可能的
PersonPair
对象。它实现了
Function<Person, List<PersonPair>>
接口,并重写了
apply()
方法。以下是该类的代码:
public class CommonPersonMapper implements Function<Person, List<PersonPair>> {
@Override
public List<PersonPair> apply(Person person) {
List<PersonPair> ret = new ArrayList<>();
List<String> contacts = person.getContacts();
Collections.sort(contacts);
for (String contact : contacts) {
PersonPair personExt = new PersonPair();
if (person.getId().compareTo(contact) < 0) {
personExt.setId(person.getId());
personExt.setOtherId(contact);
} else {
personExt.setId(contact);
personExt.setOtherId(person.getId());
}
personExt.setContacts(contacts);
ret.add(personExt);
}
return ret;
}
}
-
ConcurrentSocialNetwork类
:这是示例的主类,实现了静态方法
bidirectionalCommonContacts(),用于计算社交网络中每对联系人之间的共同联系人。内部使用两个不同的流来实现算法。以下是该类的代码:
public class ConcurrentSocialNetwork {
public static List<PersonPair> bidirectionalCommonContacts(List<Person> people) {
Map<String, List<PersonPair>> group = people.parallelStream()
.map(new CommonPersonMapper())
.flatMap(Collection::stream)
.collect(Collectors.groupingByConcurrent(PersonPair::getFullId));
Collector<Collection<String>, AtomicReference<Collection<String>>, Collection<String>> intersecting = Collector.of(
() -> new AtomicReference<>(null), (acc, list) -> {
acc.updateAndGet(set -> set == null ? new ConcurrentLinkedQueue<>(list) : set).retainAll(list);
}, (acc1, acc2) -> {
if (acc1.get() == null)
return acc2;
if (acc2.get() == null)
return acc1;
acc1.get().retainAll(acc2.get());
return acc1;
}, (acc) -> acc.get() == null ? Collections.emptySet() : acc.get(),
Collector.Characteristics.CONCURRENT, Collector.Characteristics.UNORDERED);
List<PersonPair> peopleCommonContacts = group.entrySet()
.parallelStream()
.map((entry) -> {
Collection<String> commonContacts = entry.getValue()
.parallelStream()
.map(p -> p.getContacts())
.collect(intersecting);
PersonPair person = new PersonPair();
person.setId(entry.getKey().split(",")[0]);
person.setOtherId(entry.getKey().split(",")[1]);
person.setContacts(new ArrayList<String>(commonContacts));
return person;
}).collect(Collectors.toList());
return peopleCommonContacts;
}
}
这个流的组件如下:
1. 使用输入列表的
parallelStream()
方法创建流。
2. 使用
map()
方法和
CommonPersonMapper
类将每个
Person
对象转换为
PersonPair
对象列表。
3. 使用
flatMap()
方法将
List<PersonPair>
对象流转换为
PersonPair
对象流。
4. 使用
collect()
方法和
groupingByConcurrent()
方法生成一个映射,键为用户对的标识符,值为包含两个用户联系人的
PersonPair
对象列表。
然后,创建一个新的收集器
intersecting
,用于计算共同联系人。最后,将生成的映射转换为包含每对用户共同联系人的
PersonPair
对象列表。
-
ConcurrentMain类
:该类实现了
main()方法,用于测试算法。使用两个数据集进行测试:一个简单数据集和一个基于Facebook真实数据的数据集。以下是该类的代码:
public class ConcurrentMain {
public static void main(String[] args) {
Date start, end;
System.out.println("Concurrent Main Bidirectional - Test");
List<Person> people = DataLoader.load("data", "test.txt");
start = new Date();
List<PersonPair> peopleCommonContacts = ConcurrentSocialNetwork.bidirectionalCommonContacts(people);
end = new Date();
peopleCommonContacts.forEach(p -> System.out.println(p.getFullId() + ": " + getContacts(p.getContacts())));
System.out.println("Execution Time: " + (end.getTime() - start.getTime()));
System.out.println("Concurrent Main Bidirectional - Facebook");
people = DataLoader.load("data", "facebook_contacts.txt");
start = new Date();
peopleCommonContacts = ConcurrentSocialNetwork.bidirectionalCommonContacts(people);
end = new Date();
peopleCommonContacts.forEach(p -> System.out.println(p.getFullId() + ": " + getContacts(p.getContacts())));
System.out.println("Execution Time: " + (end.getTime() - start.getTime()));
}
private static String formatContacts(List<String> contacts) {
StringBuffer buffer = new StringBuffer();
for (String contact : contacts) {
buffer.append(contact + ",");
}
return buffer.toString();
}
}
串行版本
串行版本与并发版本基本相同,但需要进行以下更改:
- 将
parallelStream()
方法替换为
stream()
方法。
- 将
ConcurrentLinkedDeque
数据结构替换为
ArrayList
数据结构。
- 将
groupingByConcurrent()
方法替换为
groupingBy()
方法。
- 不使用
of()
方法的最后一个参数。
比较两个版本
使用JMH框架对两个版本进行基准测试,在四核处理器计算机上执行10次,并计算平均执行时间。结果如下表所示:
| 示例 | Facebook |
| ---- | ---- |
| 串行 | 0.861 | 7002.485 |
| 并发 | 1.352 | 5303.990 |
可以得出以下结论:
- 对于示例数据集,串行版本执行时间更短,因为示例数据集元素较少。
- 对于Facebook数据集,并发版本执行时间更短。
并发数据结构
在并发应用程序中,数据结构的选择至关重要。如果不同线程可以修改存储在同一数据结构中的数据,则需要使用同步机制来保护该数据结构。为避免数据竞争条件,可以选择以下两种方式:
- 使用非同步数据结构,并自行添加同步机制。
- 使用Java并发API提供的内部实现同步机制并针对并发应用程序进行优化的数据结构。推荐使用第二种方式。
阻塞和非阻塞数据结构
Java并发API提供了两种并发数据结构:
-
阻塞数据结构
:当操作无法立即完成时(例如,要取出元素但数据结构为空),调用线程将被阻塞,直到操作可以完成。
-
非阻塞数据结构
:当操作无法立即完成时,返回特殊值或抛出异常。
有时,阻塞数据结构有非阻塞的等效结构。例如,
ConcurrentLinkedDeque
是非阻塞数据结构,
LinkedBlockingDeque
是其阻塞等效结构。阻塞数据结构也有类似非阻塞数据结构的方法,如
Deque
接口的
pollFirst()
方法,若双端队列为空则不阻塞并返回
null
。
接口
以下是并发数据结构实现的重要接口:
-
BlockingQueue
:队列是一种线性数据结构,允许在队列末尾插入元素,在队列开头获取元素,是先进先出(FIFO)数据结构。
JCF
定义的
Queue
接口提供了插入、检索和移除元素的基本操作,有抛出异常和返回特殊值两种版本的方法。
BlockingQueue
接口扩展了
Queue
接口,添加了阻塞方法。具体方法如下表所示:
| 操作 | 抛出异常 | 返回特殊值 | 阻塞 |
| ---- | ---- | ---- | ---- |
| 插入 | add() | offer() | put() |
| 检索和移除 | remove() | poll() | take() |
| 检索但不移除 | element() | peek() | N/A |
-
BlockingDeque
:双端队列允许在数据结构的两端插入和删除元素。
JCF
定义的
Deque
接口扩展了
Queue
接口,提供了在两端插入、检索和移除元素的方法。
BlockingDeque
接口扩展了
Deque
接口,添加了阻塞方法。具体方法如下表所示:
| 操作 | 抛出异常 | 返回特殊值 | 阻塞 |
| ---- | ---- | ---- | ---- |
| 插入 | addFirst(), addLast() | offerFirst(), offerLast() | putFirst(), putLast() |
| 检索和移除 | removeFirst(), removeLast() | pollFirst(), pollLast() | takeFirst(), takeLast() |
| 检索但不移除 | getFirst(), getLast() | peekFirst(), peekLast() | N/A |
-
ConcurrentMap
:映射允许存储键值对。
JCF
提供的
Map
接口定义了基本操作,Java 8对其进行了修改,添加了新方法,如
forEach()
、
compute()
、
computeIfAbsent()
、
computeIfPresent()
和
merge()
。
ConcurrentMap
扩展了
Map
接口,为并发应用程序提供相同的方法。
-
TransferQueue
:该接口扩展了
BlockingQueue
接口,添加了将元素从生产者传输到消费者的方法,生产者可以等待消费者取走元素。新方法包括
transfer()
和
tryTransfer()
。
类
Java并发API提供了上述接口的不同实现类:
-
LinkedBlockingQueue
:实现
BlockingQueue
接口,提供具有阻塞方法的队列,可选择限制元素数量。
-
ConcurrentLinkedQueue
:实现
Queue
接口,提供线程安全的无界队列,内部使用非阻塞算法。
-
LinkedBlockingDeque
:实现
BlockingDeque
接口,提供具有阻塞方法的双端队列,可选择限制元素数量。
-
ConcurrentLinkedDeque
:实现
Deque
接口,提供线程安全的无界双端队列,允许在两端添加和删除元素。
-
ArrayBlockingQueue
:实现
BlockingQueue
接口,基于数组提供具有固定元素数量的阻塞队列。
-
DelayQueue
:实现
BlockingDeque
接口,提供具有阻塞方法的无界队列,元素必须实现
Delayed
接口。
-
LinkedTransferQueue
:实现
TransferQueue
接口,提供无界阻塞队列,可作为生产者和消费者之间的通信通道。
-
PriorityBlockingQueue
:实现
BlockingQueue
接口,元素可根据自然顺序或构造函数中指定的比较器进行排序。
-
ConcurrentHashMap
:实现
ConcurrentMap
接口,提供线程安全的哈希表。除了Java 8中
Map
接口添加的方法外,还添加了
search()
、
searchEntries()
、
searchKeys()
和
searchValues()
方法。
综上所述,并行流和并发数据结构在处理海量数据集和并发应用程序中非常有用。通过合理选择数据结构和使用同步机制,可以避免数据竞争条件,提高程序的性能和稳定性。
并行流处理海量数据集与并发数据结构
同步机制
在并发应用程序中,同步机制同样是关键要素。它主要用于实现互斥,创建临界区,即同一时间仅允许一个线程执行的代码段;还能用于实现线程间的依赖关系,例如一个并发任务需等待另一个任务完成。Java并发API提供了从基础的
synchronized
关键字到高级实用工具(如
CyclicBarrier
类和
Phaser
类)等多种同步机制。
基础同步机制 - synchronized关键字
synchronized
关键字是Java中最基础的同步机制,它可以修饰方法或代码块,保证同一时间只有一个线程能访问被修饰的方法或代码块。以下是一个简单示例:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + example.count);
}
}
在上述代码中,
increment()
方法被
synchronized
修饰,确保了在多线程环境下
count
变量的安全更新。
高级同步工具 - CyclicBarrier类
CyclicBarrier
类允许一组线程相互等待,直到所有线程都到达某个屏障点,然后继续执行。以下是一个使用
CyclicBarrier
的示例:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("All parties have reached the barrier.");
});
for (int i = 0; i < parties; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
barrier.await();
System.out.println(Thread.currentThread().getName() + " has passed the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,创建了一个
CyclicBarrier
对象,指定了参与线程的数量为3。每个线程在执行到
barrier.await()
时会等待,直到所有3个线程都到达该点,然后继续执行后续代码。
并行流与并发数据结构的综合应用
为了更直观地展示并行流和并发数据结构的综合应用,我们来看一个简单的流程图:
graph TD;
A[开始] --> B[加载数据];
B --> C[创建并行流];
C --> D[使用CommonPersonMapper转换数据];
D --> E[使用flatMap扁平化流];
E --> F[使用groupingByConcurrent分组];
F --> G[创建收集器计算共同联系人];
G --> H[生成包含共同联系人的PersonPair列表];
H --> I[结束];
这个流程图展示了使用并行流处理社交网络数据,计算用户间共同联系人的主要步骤。
操作步骤总结
在实际应用中,使用并行流和并发数据结构时,可以按照以下步骤进行操作:
1.
数据加载
:从文件或数据库等数据源加载数据。例如,在
ConcurrentMain
类中使用
DataLoader.load()
方法加载数据。
2.
并行流处理
:
- 使用
parallelStream()
方法创建并行流。
- 使用
map()
、
flatMap()
等方法转换和处理数据。
- 使用
collect()
方法和合适的收集器进行数据聚合。
3.
并发数据结构选择
:根据具体需求选择合适的并发数据结构,如需要线程安全的队列可以选择
ConcurrentLinkedQueue
,需要线程安全的映射可以选择
ConcurrentHashMap
。
4.
同步机制使用
:如果涉及多线程对共享资源的访问,使用同步机制(如
synchronized
关键字或
CyclicBarrier
类)确保数据的一致性。
总结
通过对并行流处理海量数据集和并发数据结构及同步机制的介绍,我们了解到它们在并发编程中的重要性。并行流可以充分利用多核处理器的性能,提高数据处理效率;并发数据结构和同步机制则可以保证在多线程环境下数据的安全和一致性。在实际应用中,需要根据具体场景合理选择数据结构和同步机制,以达到最佳的性能和稳定性。
以下是一个简单的表格,总结了不同并发数据结构的特点:
| 数据结构 | 类型 | 特点 |
| ---- | ---- | ---- |
| ConcurrentLinkedQueue | 非阻塞 | 线程安全的无界队列 |
| LinkedBlockingQueue | 阻塞 | 可限制元素数量的阻塞队列 |
| ConcurrentHashMap | 非阻塞 | 线程安全的哈希表 |
| LinkedBlockingDeque | 阻塞 | 可限制元素数量的阻塞双端队列 |
希望通过本文的介绍,能帮助你更好地理解和应用并行流、并发数据结构和同步机制,在并发编程中取得更好的效果。
超级会员免费看
4466

被折叠的 条评论
为什么被折叠?



