【问题标题】:How to make writing method thread safe?如何使编写方法线程安全?
【发布时间】:2017-10-09 19:09:16
【问题描述】:

我有多个线程来调用一个方法来将内容从对象写入文件,如下所示: 当我使用 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&lt;Result&gt; results = ResultGenerator.getResult()。我真正想要的是使用多个线程来处理这个方法调用,因为这部分很昂贵并且需要很多时间。写作部分很快,我真的不需要多线程。

鉴于这种变化,有没有办法让这个方法调用在并发环境中安全?

【问题讨论】:

  • 你可以做到synchronized 但也许你应该重新考虑你的逻辑。您真的需要多个线程写入同一个文件吗?
  • 写入同一个输出目的地本质上是不安全的。解释为什么你认为你需要这样做会有所帮助;一个可能的解决方案是使用单个阅读器将文档发布到并发队列中。
  • @Gabriel,请查看我的“编辑”,并提出您的建议。
  • @chrylis 为什么它天生就不安全?
  • @user697911 如果我的回答有帮助,请告诉我。

标签: java multithreading concurrency contention


【解决方案1】:

本质上,您最终会受到单个文件的限制。没有全局变量,也没有发布任何内容,因此该方法是线程安全的。

但是,如果处理确实需要很多时间,您可以使用并行流并将结果发布到 concurrenthashmap 或阻塞队列。但是,您仍然有一个消费者可以写入文件。

【讨论】:

  • 如果他将同一个 writer 传递给多个调用,则该方法是不安全的。
  • 是的,但这本质上是不安全的。我在这里的意思是他应该将结果发布到阻塞队列,然后单个消费者应该写入文件。方法定义中的任何内容都没有假定编写器是共享的黑白调用。
  • 不,但是问题的描述加上作者通过是一个很大的危险信号。
【解决方案2】:

我不太精通 Java,所以我将提供一个与语言无关的答案。

您要做的是将矩阵转换为结果,然后将它们格式化为字符串,最后将它们全部写入流中。

目前,您会在处理每个结果后立即写入流,因此当您向逻辑添加多线程时,您最终会在流中遇到竞争条件。

您已经发现只有对 ResultGenerator.getResult() 的调用应该并行完成,而流仍然需要按顺序访问。

现在您只需将其付诸实践。按顺序做:

  • 建立一个列表,其中每个项目都是生成结果所需的内容
  • 并行处理此列表,从而生成所有结果(这是map 操作)。您的项目列表将成为结果列表。
  • 现在您已经有了结果,因此您可以按顺序迭代它们以格式化并将它们写入流中。

我怀疑 Java 8 提供了一些工具来以功能方式制作一切,但正如我所说的,我不是 Java 人,所以我不能提供代码示例。我希望这个解释就足够了。

@编辑

这个 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

【讨论】:

    【解决方案3】:

    如果您需要按预定顺序排列的最终文件,请不要使用多线程,否则您将无法获得预期的结果。

    如果您认为使用多线程您的程序在 I/O 输出方面执行得更快,那么您可能错了;由于同步导致的锁定或开销,您实际上会比单线程性能下降。

    如果您尝试编写一个非常大的文件,Document 实例的顺序无关紧要,并且您认为您的 writer 方法会遇到 CPU 瓶颈(但我可以从我们的代码中找出的唯一可能原因是frequency() 方法调用),您可以做的是让每个线程拥有自己的 BufferedWriter 写入临时文件,然后添加一个等待所有线程的附加线程,然后使用连接生成最终文件。

    【讨论】:

      【解决方案4】:

      如果您的代码使用不同的 doc 和 writer 对象,那么您的方法已经是线程安全的,因为它不访问和使用实例变量。

      如果您正在编写将相同的 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();
      }
      

      【讨论】:

        【解决方案5】:

        我会让它同步。在这种情况下,您的应用程序中只允许一个线程同时调用此方法 => 没有混乱的输出。如果您有多个应用程序正在运行,您应该考虑文件锁定之类的东西。

        同步方法示例:

        public synchronized void myMethod() {
            // ...
        }
        

        这个方法是每个线程独有的。

        【讨论】:

          【解决方案6】:

          您可以锁定一个方法,然后在完成后将其解锁。通过将同步放在方法之前,您可以确保一次只有一个线程可以执行它。同步会降低 Java 的速度,因此只应在必要时使用。

          ReentrantLock lock = new ReentrantLock();
          
           /* synchronized */ 
          public void run(){
          
              lock.lock();
          
              System.out.print("Hello!");
          
              lock.unlock();
          
           }
          

          这就像同步一样锁定方法。您可以使用它来代替同步,这就是同步在上面注释掉的原因。

          【讨论】:

            猜你喜欢
            • 2018-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-03-20
            • 2022-09-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多