【问题标题】:How to make all threads wait only when a certain method is being called如何让所有线程仅在调用某个方法时等待
【发布时间】:2015-08-10 09:03:38
【问题描述】:

上下文

我正在构建一个简单的日志记录机制。我的 Logger 有一个名为 log(String txt) 的方法,该方法必须符合两个要求:

1- 许多线程可以访问 Logger 的对象,因此线程可以根据需要调用 log() 方法。

2- 创建 Logger 的实例/对象后,将创建一个用于存储日志的文件。但是随着时间的推移,日志文件会变大,需要创建一个新文件。

这是我想要实现的简单框架。

public class Logger {

    File loggingFile;
    boolean shouldBeSaved;

    public void log(String msg) {
        if (shouldBeSaved) {
            saveLoggingFile();
        }

        //now write a message to loggingFile.
    }

    public void saveLoggingFile() {
        //close the loggingFile and create a new one.
        //update the shouldBeSaved flag somehow
    }

}

问题

现在我面临的问题是,有时一个线程进入log() 方法并尝试发送消息,而另一个线程同时进入log() 方法并关闭文件。此时我得到一个异常,因为在第一个线程尝试写入文件时文件已关闭。因此我需要同步这个。

所以当我解决问题时,我考虑过像这样在log() 方法上同步:

public synchronized void log(String msg) 

然而,需要使用这种日志机制的应用程序有很多线程,并且它需要尽快记录,因为它需要非常快速地响应。因此,像这样同步会使很多线程等待获取锁。我一直在寻找更好,更优雅的解决方案,我希望有人能提供帮助。我能想到的解决这个问题的方法是让所有其他线程等待只有在saveLoggingFile()被调用时。在saveLoggingFile() 上同步不会解决问题,因为一个线程可以尝试写入,同时另一个线程进入并尝试关闭文件。

我相信我正在寻找的这种同步具有某种模式或名称,因此如果您也能提供该模式的名称,我将不胜感激。

我不想使用日志框架,但我想知道如何解决这样的问题,因为我可能会在不同的上下文中遇到类似的问题。

【问题讨论】:

  • 使用日志框架。学习如何使用库和正确记录日志会比以这种方式摸索东西更好。
  • 你的 log 方法不应该直接调用 IO 操作,而是将期望记录的消息放入 Queue。异步 saveLoggingFile 从 Queue 读取消息并存储到文件(在 1 个线程中)。
  • 我完全同意马特的观点。你应该使用一个登录框架来代替你处理所有这些事情,或者重新发明轮子来创建一个新的日志工具。
  • 一种解决方案是让您的log() 函数将请求推送到队列而不是直接写入日志文件,并有一个后台线程为队列提供服务并写入文件。但@matt 和 Ricardo 所说的更重要。如果您不将此作为学习练习(即,如果您需要一个记录器),那么您应该使用一个已经可用的记录器。不要重新发明轮子。
  • 你试过我建议的答案了吗?这将让您登录,而无需尝试记录需要阻塞的线程?

标签: java multithreading logging


【解决方案1】:

使用队列。

public class LoggingQueue{
    BlockingQueue<String> messagesToPost = new LinkedBlockingQueue();
    public void run(){
        while(!Thread.interrupted()){
            try{
                String s = messagesToPost.take();
                postMessage(String s);
            } catch(InterruptedException e){
                //do what you need to.
            }
        }
    }
    private void postMessage(String s){
      //do your blocking io stuff here. It is only run on the logging thread.
    }

    public void log(String s){
         try{
             messagesToPost.put(s);
         }catch(InterruptedException e){
            //this is not a good scene.
         }
    }
}

如下所述,可以使用 cmets ArrayBlockingQueue 代替链接版本。这可以防止队列消耗您的所有内存。然后可以使用诸如 offer 之类的东西来代替 put。当队列已满时,优惠不会阻塞。如果以某种方式过于频繁地生成错误,这将很重要。 IO 很慢,但不会慢到您应该有 100 多条错误消息排队。如果发生这种情况,您可能需要引发某种日志记录异常。

您还可以更聪明地轮询队列以写入消息,这样如果有几条消息,您可以在一个打开/写入操作中将它们全部发布。

每个 catch 块都需要妥善管理,因为这是日志记录,它应该是最后失败的事情之一。

【讨论】:

  • A LinkedBlockingQueue 可能会消耗所有内存。也许应该提到ArrayBlockingQueue 作为替代。
  • @OldCurmudgeon 你说的是old issue with memory leak吗?还是关于LinkedBlockingQueue 的其他内容?请澄清。
  • @Aivean - LinkedBlockingQueue 可以永远增长,因此,在重负载下,可能会导致此方法使系统崩溃。 ArrayBlockingQueue 具有固定的最大大小,如果尝试超过此限制,则会阻塞。阻止比崩溃更好?
  • @OldCurmudgeon 如果问题只是关于容量限制,LinkedBlockingQueueprovides this ability
【解决方案2】:

实现您提到的逻辑的常用方法是使用CountDownLatchCyclicBarrier。它们主要是为这种用途而设计的。简单地在课堂上做

private final CountDownLatch countDownLatch = new CountDownLatch(1);

然后在等待线程中:

public void thisCallFromWaitThread() {
    this.countDownLatch.wait();
}

在等待的任务中:

public void thisDoesStuffToBeWaited() {
    try {
        // do your logic
    } finally {
        this.countDownLatch.countDown();
    }
}

================================================ =======================

我再次阅读了您的问题,我认为使用基于队列的解决方案是有效的。您的所有log()saveLogFile() 都是提交到由单个线程操作的队列的操作。

private final ExecutorService service = Executors.singleThreadExecutor();

public void log(String info) {
    service.submit(new Runnable() {

        @Override
        public void run() {
            file.log(info);
        }
    });
}

public void saveLogFile() {
    service.submit(new Runnable() {

        @Override
        public void run() {
            file.saveContent(); // or any your file saving logic
        }
    });
}

请注意,通常当您想要最小化阻塞时,这是最好的方法。如果您可以接受一些阻塞,那么读写锁就足够了。将其与双缓冲区相结合以最大限度地减少锁定。

【讨论】:

    【解决方案3】:

    如答案中所述,如果您喜欢顺序访问,则可以使用CountDownLatch,但如果您只想在每次线程调用此方法时锁定访问,您只需使用Lock

       private Lock lock=new ReentrantLock();
        ....
     public void log(String msg) {
        lock.lock(); //in this stage the first thread will acquires this lock the others will wait
        {
            .... //do stuff
        }finally{
            lock.unlock(); //release the lock
        }
     }
    

    【讨论】:

    • 您所描述的与消息签名中的synchronized 相同......它会减慢速度,因为线程会争夺线程。
    • 是的,但这是所有锁的情况,锁定的目的是防止访问临界区,并且锁如 javaDoc 中所述:锁实现提供了比可以获得的更广泛的锁定操作使用同步的方法和语句。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的 Condition 对象。
    【解决方案4】:

    仅在必要时锁定。

    volatile File loggingFile;
    volatile boolean shouldBeSaved;
    
    private synchronized void checkAndRotateLogFile() {
      if ( shouldBeSaved ) {
        saveLoggingFile();
        loggingFile = newLoggingFile;
      }
    }
    
    public void log(String msg) {
      checkAndRotateLogFile();
      // Write message to loggingFile
    }
    

    这里,只要不需要轮换日志文件,同步锁就会被短暂地持有以检查if ( shouldBeSaved ) {。这是尽可能高效的。当文件关闭/切换时,所有日志记录将暂停,直到完成并获得新的一致状态(即新的打开日志文件)。

    正如其他人所写的那样,在这种情况下有多种方法可以处理同步,出于性能原因,基于队列的方法是最好的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多