如何使写方法线程安全?
How to make writing method thread safe?
我有多个线程调用一个方法将内容从对象写入文件,如下所示:
当我使用 1 个线程测试此方法时,预期会输出到我的文件中。但是,对于多线程,输出到文件中是杂乱无章的。如何使这个线程安全?
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
writer.write(matrix, matrix.frequency());
writer.write(results.toString());
writer.write("\n");
}
}
}
编辑:
我添加了这一行 List<Result> results = ResultGenerator.getResult()
。我真正想要的是使用多个线程来处理这个方法调用,因为这部分很昂贵并且需要很多时间。写作部分很快,我真的不需要多线程。
鉴于此更改,有没有办法使此方法调用在并发环境中安全?
我会让它同步。在那种情况下,您的应用程序中只允许一个线程同时调用此方法 => 没有混乱的输出。如果你有多个应用程序 运行,你应该考虑像文件锁定这样的东西。
同步方法示例:
public synchronized void myMethod() {
// ...
}
该方法每个线程独享
您可以锁定一个方法,然后在完成时解锁它。通过将 synchronized 放在方法之前,您可以确保一次只有一个线程可以执行它。同步会减慢 Java,因此只应在必要时使用。
ReentrantLock lock = new ReentrantLock();
/* synchronized */
public void run(){
lock.lock();
System.out.print("Hello!");
lock.unlock();
}
这就像同步一样锁定方法。您可以使用它代替同步,这就是上面注释掉同步的原因。
如果您需要按预定顺序排列的最终文件,请不要使用多线程,否则您将得不到预期的结果。
如果您认为使用多线程您的程序在 I/O 输出方面会执行得更快,那您很可能错了;由于同步导致的锁定或开销,与单线程相比,您实际上会降低性能。
如果您尝试写入一个非常大的文件,Document
个实例的顺序不相关,您认为您的编写器方法会遇到 CPU 瓶颈(但唯一可能的原因我可以从我们的代码中弄清楚是 frequency()
方法调用),你可以做的是让每个线程拥有自己的 BufferedWriter 写入一个临时文件,然后添加一个额外的线程等待所有,然后生成使用串联的最终文件。
我不太精通 Java 所以我将提供一个与语言无关的答案。
你要做的是将矩阵转化为结果,然后格式化为字符串,最后全部写入流中。
目前,您在处理每个结果后立即写入流,因此当您向逻辑中添加多线程时,您最终会在流中出现竞争条件。
您已经知道只有 ResultGenerator.getResult()
的调用应该并行完成,而流仍然需要顺序访问。
现在你只需要付诸实践。按顺序做:
- 构建一个列表,其中每个项目都是生成结果所需要的
- 并行处理此列表,从而生成所有结果(这是一个
map
操作)。您的项目列表将成为结果列表。
- 现在您已经有了结果,因此您可以按顺序迭代它们以格式化并将它们写入流中。
我怀疑 Java 8 提供了一些工具来以功能方式制作所有内容,但如前所述,我不是 Java 人,因此我无法提供代码示例。我希望这个解释足够了。
@edit
这个 F# 示例代码解释了我的意思。
open System
// This is a pretty long and nasty operation!
let getResult doc =
Threading.Thread.Sleep(1000)
doc * 10
// This is writing into stdout, but it could be a stream...
let formatAndPrint =
printfn "Got result: %O"
[<EntryPoint>]
let main argv =
printfn "Starting..."
[| 1 .. 10 |] // A list with some docs to be processed
|> Array.Parallel.map getResult // Now that's doing the trick
|> Array.iter formatAndPrint
0
从本质上讲,您最终会受到单个文件的限制。没有全局变量,也没有发布任何内容,因此该方法是线程安全的。
但是,如果处理 确实需要很多时间,您可以使用并行流并将结果发布到并发哈希映射或阻塞队列。但是,您仍然会有一个消费者写入文件。
如果您的代码使用不同的 doc 和 writer 对象,那么您的方法已经是线程安全的,因为它不访问和使用实例变量。
如果您将同一个编写器对象传递给该方法,您可以根据需要使用以下方法之一:
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
// ensure that no other thread interferes while the following
// three .write() statements are executed.
synchronized(writer) {
writer.write(matrix, matrix.frequency()); // from your example, but I doubt it compiles
writer.write(results.toString());
writer.write("\n");
}
}
}
}
或使用临时 StringBuilder 对象无锁:
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
StringBuilder sb = new StringBuilder();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
sb.append(matrix).append(matrix.frequency());
sb.append(results.toString());
sb.append("n");
}
}
// write everything at once
writer.write(sb.toString();
}
我有多个线程调用一个方法将内容从对象写入文件,如下所示: 当我使用 1 个线程测试此方法时,预期会输出到我的文件中。但是,对于多线程,输出到文件中是杂乱无章的。如何使这个线程安全?
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
writer.write(matrix, matrix.frequency());
writer.write(results.toString());
writer.write("\n");
}
}
}
编辑:
我添加了这一行 List<Result> results = ResultGenerator.getResult()
。我真正想要的是使用多个线程来处理这个方法调用,因为这部分很昂贵并且需要很多时间。写作部分很快,我真的不需要多线程。
鉴于此更改,有没有办法使此方法调用在并发环境中安全?
我会让它同步。在那种情况下,您的应用程序中只允许一个线程同时调用此方法 => 没有混乱的输出。如果你有多个应用程序 运行,你应该考虑像文件锁定这样的东西。
同步方法示例:
public synchronized void myMethod() {
// ...
}
该方法每个线程独享
您可以锁定一个方法,然后在完成时解锁它。通过将 synchronized 放在方法之前,您可以确保一次只有一个线程可以执行它。同步会减慢 Java,因此只应在必要时使用。
ReentrantLock lock = new ReentrantLock();
/* synchronized */
public void run(){
lock.lock();
System.out.print("Hello!");
lock.unlock();
}
这就像同步一样锁定方法。您可以使用它代替同步,这就是上面注释掉同步的原因。
如果您需要按预定顺序排列的最终文件,请不要使用多线程,否则您将得不到预期的结果。
如果您认为使用多线程您的程序在 I/O 输出方面会执行得更快,那您很可能错了;由于同步导致的锁定或开销,与单线程相比,您实际上会降低性能。
如果您尝试写入一个非常大的文件,Document
个实例的顺序不相关,您认为您的编写器方法会遇到 CPU 瓶颈(但唯一可能的原因我可以从我们的代码中弄清楚是 frequency()
方法调用),你可以做的是让每个线程拥有自己的 BufferedWriter 写入一个临时文件,然后添加一个额外的线程等待所有,然后生成使用串联的最终文件。
我不太精通 Java 所以我将提供一个与语言无关的答案。
你要做的是将矩阵转化为结果,然后格式化为字符串,最后全部写入流中。
目前,您在处理每个结果后立即写入流,因此当您向逻辑中添加多线程时,您最终会在流中出现竞争条件。
您已经知道只有 ResultGenerator.getResult()
的调用应该并行完成,而流仍然需要顺序访问。
现在你只需要付诸实践。按顺序做:
- 构建一个列表,其中每个项目都是生成结果所需要的
- 并行处理此列表,从而生成所有结果(这是一个
map
操作)。您的项目列表将成为结果列表。 - 现在您已经有了结果,因此您可以按顺序迭代它们以格式化并将它们写入流中。
我怀疑 Java 8 提供了一些工具来以功能方式制作所有内容,但如前所述,我不是 Java 人,因此我无法提供代码示例。我希望这个解释足够了。
@edit
这个 F# 示例代码解释了我的意思。
open System
// This is a pretty long and nasty operation!
let getResult doc =
Threading.Thread.Sleep(1000)
doc * 10
// This is writing into stdout, but it could be a stream...
let formatAndPrint =
printfn "Got result: %O"
[<EntryPoint>]
let main argv =
printfn "Starting..."
[| 1 .. 10 |] // A list with some docs to be processed
|> Array.Parallel.map getResult // Now that's doing the trick
|> Array.iter formatAndPrint
0
从本质上讲,您最终会受到单个文件的限制。没有全局变量,也没有发布任何内容,因此该方法是线程安全的。
但是,如果处理 确实需要很多时间,您可以使用并行流并将结果发布到并发哈希映射或阻塞队列。但是,您仍然会有一个消费者写入文件。
如果您的代码使用不同的 doc 和 writer 对象,那么您的方法已经是线程安全的,因为它不访问和使用实例变量。
如果您将同一个编写器对象传递给该方法,您可以根据需要使用以下方法之一:
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
// ensure that no other thread interferes while the following
// three .write() statements are executed.
synchronized(writer) {
writer.write(matrix, matrix.frequency()); // from your example, but I doubt it compiles
writer.write(results.toString());
writer.write("\n");
}
}
}
}
或使用临时 StringBuilder 对象无锁:
void (Document doc, BufferedWriter writer){
Map<Sentence, Set<Matrix>> matrix = doc.getMatrix();
StringBuilder sb = new StringBuilder();
for(Sentence sentence : matrix.keySet()){
Set<Matrix> set = doc.getMatrix(sentence);
for(Matrix matrix : set){
List<Result> results = ResultGenerator.getResult();
sb.append(matrix).append(matrix.frequency());
sb.append(results.toString());
sb.append("n");
}
}
// write everything at once
writer.write(sb.toString();
}