深入并发数据结构与同步工具及文档聚类应用实现
1. 并发应用的关键组件
并发应用通常包含两个重要组件:数据结构和同步机制。
-
数据结构
:每个程序都会利用数据结构在内存中存储待处理的信息。在 Java 8 并发 API 中,对
ConcurrentHashMap
类以及实现
Collection
接口的类引入了新特性。
-
同步机制
:当多个并发任务想要修改数据时,同步机制可保护数据;必要时,还能控制任务的执行顺序。Java 8 并发 API 中的
CompletableFuture
就是这样一种新特性。
2. 大型组件同步机制
大型计算机应用由多个组件协同工作以实现所需功能,这些组件之间需要进行同步和通信。当要同步的组件本身也是并发系统,且可能使用不同机制实现并发时,任务组织会更加复杂。此时,可以使用以下两种机制进行同步和通信:
-
共享内存
:系统通过共享数据结构来传递信息。
-
消息传递
:一个系统向一个或多个系统发送消息,有以下两种类型:
-
同步消息传递
:发送消息的类会等待接收者处理完消息。
-
异步消息传递
:发送消息的类不会等待接收者处理消息。
下面是这两种机制的对比表格:
| 机制 | 特点 | 示例 |
| — | — | — |
| 共享内存 | 系统共享数据结构传递信息 | 多个任务共享同一个
Vocabulary
类实例 |
| 消息传递 - 同步 | 发送方等待接收方处理消息 | 无 |
| 消息传递 - 异步 | 发送方不等待接收方处理消息 | 读者系统读取文档后写入缓冲区,不等待处理 |
3. 文档聚类应用示例
该应用将读取一组文档,并使用 k - means 聚类算法对其进行组织,主要包含四个组件:
-
Reader 系统
:读取所有文档,并将每个文档转换为
String
对象列表。
-
Indexer 系统
:处理文档,将其转换为单词列表,同时生成包含所有出现单词的全局词汇表。
-
Mapper 系统
:使用向量空间模型将每个单词列表转换为数学表示,每个项目的值为 Tf - Idf 度量。
-
Clustering 系统
:使用 k - means 聚类算法对文档进行聚类。
下面是这四个系统的工作流程 mermaid 流程图:
graph LR
A[Reader 系统] --> B[Indexer 系统]
B --> C[Mapper 系统]
C --> D[Clustering 系统]
4. k - means 聚类的四个系统实现
4.1 Reader 系统
该系统在
DocumentReader
类中实现,该类实现了
Runnable
接口,内部使用三个属性:
-
ConcurrentLinkedDeque<String>
:包含要处理的所有文件名。
-
ConcurrentLinkedQueue<TextFile>
:用于存储文档。
-
CountDownLatch
:控制任务执行的结束。
以下是
DocumentReader
类的代码:
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DocumentReader implements Runnable {
private ConcurrentLinkedDeque<String> files;
private ConcurrentLinkedQueue<TextFile> buffer;
private CountDownLatch readersCounter;
public DocumentReader(ConcurrentLinkedDeque<String> files, ConcurrentLinkedQueue<TextFile> buffer, CountDownLatch readersCounter) {
this.files = files;
this.buffer = buffer;
this.readersCounter = readersCounter;
}
@Override
public void run() {
String route;
System.out.println(Thread.currentThread().getName() + ": Reader start");
while ((route = files.pollFirst()) != null) {
Path file = Paths.get(route);
TextFile textFile;
try {
textFile = new TextFile(file);
buffer.offer(textFile);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": Reader end: " + buffer.size());
readersCounter.countDown();
}
}
class TextFile {
private String fileName;
private java.util.List<String> content;
public TextFile(String fileName, java.util.List<String> content) {
this.fileName = fileName;
this.content = content;
}
public TextFile(java.nio.file.Path path) throws java.io.IOException {
this(path.getFileName().toString(), java.nio.file.Files.readAllLines(path));
}
public String getFileName() {
return fileName;
}
public java.util.List<String> getContent() {
return content;
}
}
4.2 Indexer 系统
该系统在
Indexer
类中实现,该类也实现了
Runnable
接口,内部使用五个属性:
-
ConcurrentLinkedQueue<TextFile>
:包含所有文档的内容。
-
ConcurrentLinkedDeque<Document>
:存储每个文档的单词列表。
-
CountDownLatch
:控制 Reader 系统的结束。
-
CountDownLatch
:指示该系统任务的结束。
-
Vocabulary
:存储文档集合的所有单词。
以下是
Indexer
类的代码:
import java.text.Normalizer;
import java.util.StringTokenizer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentLinkedDeque;
public class Indexer implements Runnable {
private ConcurrentLinkedQueue<TextFile> buffer;
private ConcurrentLinkedDeque<Document> documents;
private CountDownLatch readersCounter;
private CountDownLatch indexersCounter;
private Vocabulary voc;
public Indexer(ConcurrentLinkedDeque<Document> documents, ConcurrentLinkedQueue<TextFile> buffer, CountDownLatch readersCounter, CountDownLatch indexersCounter, Vocabulary voc) {
this.buffer = buffer;
this.documents = documents;
this.readersCounter = readersCounter;
this.indexersCounter = indexersCounter;
this.voc = voc;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": Indexer start");
do {
TextFile textFile = buffer.poll();
if (textFile != null) {
Document document = parseDoc(textFile);
document.getVoc().values().forEach(voc::addWord);
documents.offer(document);
}
} while ((readersCounter.getCount() > 0) || (!buffer.isEmpty()));
indexersCounter.countDown();
System.out.println(Thread.currentThread().getName() + ": Indexer end");
}
private Document parseDoc(TextFile textFile) {
Document doc = new Document();
doc.setName(textFile.getFileName());
textFile.getContent().forEach(line -> parseLine(line, doc));
return doc;
}
private static void parseLine(String inputLine, Document doc) {
String line = new String(inputLine);
line = Normalizer.normalize(line, Normalizer.Form.NFKD);
line = line.replaceAll("[^\\p{ASCII}]", "");
line = line.toLowerCase();
StringTokenizer tokenizer = new StringTokenizer(line, " ,.;:-{}[]¿?¡!|\\=*+/()\"@\t~#<>", false);
while (tokenizer.hasMoreTokens()) {
doc.addWord(tokenizer.nextToken());
}
}
}
class Document {
private String name;
private java.util.Map<String, Word> voc;
public Document() {
this.voc = new java.util.HashMap<>();
}
public void setName(String name) {
this.name = name;
}
public java.util.Map<String, Word> getVoc() {
return voc;
}
public void addWord(String word) {
if (!voc.containsKey(word)) {
voc.put(word, new Word());
}
voc.get(word).increment();
}
}
class Word {
private int df;
private int tfxidf;
public void increment() {
df++;
}
public int getDf() {
return df;
}
public int getTfxidf() {
return tfxidf;
}
public void setTfxidf(int tfxidf) {
this.tfxidf = tfxidf;
}
}
class Vocabulary {
private java.util.Map<String, Word> vocabulary;
public Vocabulary() {
this.vocabulary = new java.util.HashMap<>();
}
public void addWord(Word word) {
// 实现添加单词逻辑
}
public java.util.Map<String, Word> getVocabulary() {
return vocabulary;
}
}
4.3 Mapper 系统
该系统在
Mapper
类中实现,该类实现了
Runnable
接口,内部使用两个属性:
-
ConcurrentLinkedDeque<Document>
:包含所有文档的信息。
-
Vocabulary
:包含整个集合的所有单词。
以下是
Mapper
类的代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.ConcurrentLinkedDeque;
public class Mapper implements Runnable {
private ConcurrentLinkedDeque<Document> documents;
private Vocabulary voc;
public Mapper(ConcurrentLinkedDeque<Document> documents, Vocabulary voc) {
this.documents = documents;
this.voc = voc;
}
@Override
public void run() {
Document doc;
int counter = 0;
System.out.println(Thread.currentThread().getName() + ": Mapper start");
while ((doc = documents.pollFirst()) != null) {
counter++;
java.util.List<Attribute> attributes = new ArrayList<>();
doc.getVoc().forEach((key, item) -> {
Word word = voc.getWord(key);
item.setTfxidf(item.getTfxidf() / word.getDf());
Attribute attribute = new Attribute();
attribute.setIndex(word.getIndex());
attribute.setValue(item.getTfxidf());
attributes.add(attribute);
});
Collections.sort(attributes);
doc.setExample(attributes);
}
System.out.println(Thread.currentThread().getName() + ": Mapper end: " + counter);
}
}
class Attribute {
private int index;
private int value;
public void setIndex(int index) {
this.index = index;
}
public void setValue(int value) {
this.value = value;
}
}
4.4 Clustering 系统
该系统实现了 k - means 聚类算法,使用了以下元素:
-
DistanceMeasurer
类:计算文档属性数组与聚类质心之间的欧几里得距离。
-
DocumentCluster
类:存储聚类信息,包括质心和文档。
-
AssigmentTask
类:继承
RecursiveAction
类,执行算法的分配任务。
-
UpdateTask
类:继承
RecursiveAction
类,执行算法的更新任务。
-
ConcurrentKMeans
类:包含静态方法
calculate()
执行聚类算法。
此外,还添加了
ClusterTask
类,实现
Runnable
接口,调用
ConcurrentKMeans
类的
calculate()
方法。
以下是
ClusterTask
类的代码:
import java.util.concurrent.ForkJoinPool;
public class ClusterTask implements Runnable {
private Document[] documents;
private Vocabulary voc;
public ClusterTask(Document[] documents, Vocabulary voc) {
this.documents = documents;
this.voc = voc;
}
@Override
public void run() {
System.out.println("Documents to cluster: " + documents.length);
ConcurrentKMeans.calculate(documents, 10, voc.getVocabulary().size(), 991, 10);
}
}
class ConcurrentKMeans {
public static DocumentCluster[] calculate(Document[] documents, int numClusters, int vocabSize, int seed, int minSize) {
// 实现聚类算法逻辑
return null;
}
}
class DocumentCluster {
private Attribute[] centroid;
private java.util.List<Document> documents;
public DocumentCluster() {
this.documents = new java.util.ArrayList<>();
}
public void setCentroid(Attribute[] centroid) {
this.centroid = centroid;
}
public java.util.List<Document> getDocuments() {
return documents;
}
}
class DistanceMeasurer {
public static double calculateDistance(Attribute[] docAttributes, Attribute[] centroid) {
// 实现距离计算逻辑
return 0;
}
}
5. 文档聚类应用的主类
主类
ClusteringDocs
负责启动各个系统并创建同步所需的元素。以下是
ClusteringDocs
类的代码:
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
public class ClusteringDocs {
private static int NUM_READERS = 2;
private static int NUM_WRITERS = 4;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
ConcurrentLinkedDeque<String> files = readFiles("data");
System.out.println(new Date() + ":" + files.size() + " files read.");
ConcurrentLinkedQueue<List<String>> buffer = new ConcurrentLinkedQueue<>();
CountDownLatch readersCounter = new CountDownLatch(2);
ConcurrentLinkedDeque<Document> documents = new ConcurrentLinkedDeque<>();
CountDownLatch indexersCounter = new CountDownLatch(4);
Vocabulary voc = new Vocabulary();
System.out.println(new Date() + ":" + "Launching the tasks");
for (int i = 0; i < NUM_READERS; i++) {
DocumentReader reader = new DocumentReader(files, buffer, readersCounter);
executor.execute(reader);
}
for (int i = 0; i < NUM_WRITERS; i++) {
Indexer indexer = new Indexer(documents, buffer, readersCounter, indexersCounter, voc);
executor.execute(indexer);
}
System.out.println(new Date() + ":" + "Waiting for the readers");
readersCounter.await();
System.out.println(new Date() + ":" + "Waiting for the indexers");
indexersCounter.await();
Document[] documentsArray = new Document[documents.size()];
documentsArray = documents.toArray(documentsArray);
System.out.println(new Date() + ":" + "Launching the mappers");
CompletableFuture<Void>[] completables = Stream.generate(() -> new Mapper(documents, voc))
.limit(4)
.map(CompletableFuture::runAsync)
.toArray(CompletableFuture[]::new);
System.out.println(new Date() + ":" + "Launching the cluster calculation");
CompletableFuture<Void> completableMappers = CompletableFuture.allOf(completables);
ClusterTask clusterTask = new ClusterTask(documentsArray, voc);
CompletableFuture<Void> completableClustering = completableMappers.thenRunAsync(clusterTask);
System.out.println(new Date() + ":" + "Wating for the cluster calculation");
try {
completableClustering.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println(new Date() + ":" + "Execution finished");
executor.shutdown();
}
private static ConcurrentLinkedDeque<String> readFiles(String path) {
// 实现读取文件路径逻辑
return null;
}
}
6. 测试文档聚类应用
为了测试该应用,使用了从维基百科获取的 100,673 篇电影相关文档中的 10,052 篇文档作为文档集合。通过观察执行过程中的输出信息,可以验证应用的正确性。
以上就是一个完整的文档聚类应用的实现,通过并发编程和同步机制,提高了应用的性能和效率。
深入并发数据结构与同步工具及文档聚类应用实现
7. 代码优化建议
在实现文档聚类应用的过程中,我们可以对部分代码进行优化,以提高性能和可维护性。以下是一些具体的优化建议:
7.1 正则表达式预编译
在
Indexer
类的
parseLine
方法中,使用正则表达式
replaceAll("[^\\p{ASCII}]", "")
来清理字符串。为了提高性能,可以将正则表达式预编译,避免每次调用时都进行编译。优化后的代码如下:
import java.text.Normalizer;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentLinkedDeque;
public class Indexer implements Runnable {
private static final Pattern NON_ASCII = Pattern.compile("[^\\p{ASCII}]");
// 其他属性和构造函数保持不变
private static void parseLine(String inputLine, Document doc) {
String line = new String(inputLine);
line = Normalizer.normalize(line, Normalizer.Form.NFKD);
line = NON_ASCII.matcher(line).replaceAll("");
line = line.toLowerCase();
StringTokenizer tokenizer = new StringTokenizer(line, " ,.;:-{}[]¿?¡!|\\=*+/()\"@\t~#<>", false);
while (tokenizer.hasMoreTokens()) {
doc.addWord(tokenizer.nextToken());
}
}
}
7.2 线程池配置优化
在
ClusteringDocs
类中,使用了
Executors.newCachedThreadPool()
来创建线程池。该线程池会根据需要创建新线程,可能会导致系统资源耗尽。可以根据实际情况,使用
ThreadPoolExecutor
来手动配置线程池的参数,例如核心线程数、最大线程数等。优化后的代码如下:
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
public class ClusteringDocs {
private static int NUM_READERS = 2;
private static int NUM_WRITERS = 4;
public static void main(String[] args) throws InterruptedException {
// 手动配置线程池
ExecutorService executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, TimeUnit.SECONDS, // 线程空闲时间
new java.util.concurrent.LinkedBlockingQueue<>() // 任务队列
);
// 其他代码保持不变
}
}
8. 不同并发技术的使用与同步
在文档聚类应用中,我们使用了多种并发技术,如线程池、
CountDownLatch
、
CompletableFuture
等。这些技术的使用和同步关系如下表所示:
| 并发技术 | 使用场景 | 同步方式 |
| — | — | — |
| 线程池(
ThreadPoolExecutor
) | 执行多个并发任务,如 Reader、Indexer、Mapper 等系统的任务 | 无,通过线程池管理线程的生命周期 |
|
CountDownLatch
| 控制 Reader 和 Indexer 系统的结束 | 主线程调用
await()
方法等待计数器归零 |
|
CompletableFuture
| 同步 Mapper 系统的结束和 Clustering 系统的开始 | 使用
allOf()
和
thenRunAsync()
方法实现异步同步 |
下面是这些并发技术在应用中的交互 mermaid 流程图:
graph LR
A[线程池] --> B[Reader 系统]
A --> C[Indexer 系统]
B --> D[CountDownLatch - Reader]
C --> D[CountDownLatch - Reader]
D --> E[CountDownLatch - Indexer]
E --> F[Mapper 系统 - CompletableFuture]
F --> G[CompletableFuture - allOf]
G --> H[Clustering 系统 - CompletableFuture]
9. 实现替代方案探讨
除了上述实现方式,我们还可以使用 Java 并发 API 的其他组件来实现文档聚类应用。以下是一些可能的替代方案:
9.1 使用
ScheduledExecutorService
替代
CountDownLatch
ScheduledExecutorService
可以用于定时任务和延迟任务,也可以用于控制任务的执行顺序。我们可以使用
ScheduledExecutorService
来替代
CountDownLatch
控制 Reader 和 Indexer 系统的结束。示例代码如下:
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
public class ClusteringDocsAlternative {
private static int NUM_READERS = 2;
private static int NUM_WRITERS = 4;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = new ScheduledThreadPoolExecutor(5);
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
ConcurrentLinkedDeque<String> files = readFiles("data");
System.out.println(new Date() + ":" + files.size() + " files read.");
ConcurrentLinkedQueue<List<String>> buffer = new ConcurrentLinkedQueue<>();
ConcurrentLinkedDeque<Document> documents = new ConcurrentLinkedDeque<>();
Vocabulary voc = new Vocabulary();
System.out.println(new Date() + ":" + "Launching the tasks");
for (int i = 0; i < NUM_READERS; i++) {
DocumentReader reader = new DocumentReader(files, buffer);
executor.execute(reader);
}
// 延迟执行 Indexer 系统,确保 Reader 系统完成
scheduler.schedule(() -> {
for (int i = 0; i < NUM_WRITERS; i++) {
Indexer indexer = new Indexer(documents, buffer, voc);
executor.execute(indexer);
}
}, 5, TimeUnit.SECONDS);
// 后续代码保持不变
}
private static ConcurrentLinkedDeque<String> readFiles(String path) {
// 实现读取文件路径逻辑
return null;
}
}
9.2 使用
CyclicBarrier
替代
CompletableFuture
CyclicBarrier
可以用于多个线程在某个点上同步,等待所有线程都到达该点后再继续执行。我们可以使用
CyclicBarrier
来替代
CompletableFuture
同步 Mapper 系统的结束和 Clustering 系统的开始。示例代码如下:
import java.util.Date;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
public class ClusteringDocsAlternative2 {
private static int NUM_READERS = 2;
private static int NUM_WRITERS = 4;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
ConcurrentLinkedDeque<String> files = readFiles("data");
System.out.println(new Date() + ":" + files.size() + " files read.");
ConcurrentLinkedQueue<List<String>> buffer = new ConcurrentLinkedQueue<>();
ConcurrentLinkedDeque<Document> documents = new ConcurrentLinkedDeque<>();
Vocabulary voc = new Vocabulary();
// 创建 CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(4, () -> {
Document[] documentsArray = new Document[documents.size()];
documentsArray = documents.toArray(documentsArray);
ClusterTask clusterTask = new ClusterTask(documentsArray, voc);
executor.execute(clusterTask);
});
System.out.println(new Date() + ":" + "Launching the tasks");
for (int i = 0; i < NUM_READERS; i++) {
DocumentReader reader = new DocumentReader(files, buffer);
executor.execute(reader);
}
for (int i = 0; i < NUM_WRITERS; i++) {
Indexer indexer = new Indexer(documents, buffer, voc);
executor.execute(indexer);
}
// 启动 Mapper 系统
System.out.println(new Date() + ":" + "Launching the mappers");
for (int i = 0; i < 4; i++) {
Mapper mapper = new Mapper(documents, voc, barrier);
executor.execute(mapper);
}
// 后续代码保持不变
}
private static ConcurrentLinkedDeque<String> readFiles(String path) {
// 实现读取文件路径逻辑
return null;
}
}
class Mapper implements Runnable {
private ConcurrentLinkedDeque<Document> documents;
private Vocabulary voc;
private CyclicBarrier barrier;
public Mapper(ConcurrentLinkedDeque<Document> documents, Vocabulary voc, CyclicBarrier barrier) {
this.documents = documents;
this.voc = voc;
this.barrier = barrier;
}
@Override
public void run() {
// 实现 Mapper 逻辑
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
10. 总结
通过实现文档聚类应用,我们深入了解了并发数据结构和同步工具在实际项目中的应用。使用并发编程可以显著提高应用的性能和效率,但同时也需要注意同步和通信的问题。在实际开发中,我们可以根据具体需求选择合适的并发技术和同步机制,同时对代码进行优化,以确保应用的稳定性和可维护性。
此外,我们还探讨了不同的实现替代方案,展示了 Java 并发 API 的灵活性和多样性。在选择实现方案时,需要综合考虑性能、可维护性、代码复杂度等因素,以达到最佳的开发效果。
希望本文能为你在并发编程和文档聚类应用开发方面提供一些有价值的参考。
超级会员免费看
17万+

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



