【问题标题】:Java: Reading the same file from different threads sometimes returns null content in some of the threadsJava:从不同线程读取同一个文件有时会在某些线程中返回空内容
【发布时间】:2020-02-04 03:37:35
【问题描述】:

首先,我是编码新手,所以对于我可能犯的任何错误,我深表歉意

我正在使用 Java (openJDK11)Spring boot 开发后端服务器:

该应用程序由许多面板和子面板组成,这些面板和子面板可以从 Web 浏览器中打开。单击子面板时,前端会执行三个不同的 GET 请求。

这三个请求期望不同的响应(json 架构、json 数据等)。每个请求都会启动一个访问同一个配置文件(每个子面板有一个配置文件)的不同线程,该线程会被解析。读取配置文件后,每个线程执行不同的操作,它们的共同点只有 config-reader 部分。

  • 有时(这让我想到了并发),其中一个/一些线程中的读取操作无法执行,因为bufferedReader.readLine() 在不读取任何行的情况下返回 null

  • 另外,有时会发生在正确读取某些行后,bufferedReader.readLine() 突然返回 null,但文件尚未完全读取。

每个线程创建一个本地InputStream 来打开文件,并创建一个本地BufferedReader 来解析它。

我已经尝试使同步 parseFile 方法(虽然我觉得这是不对的,因为我不希望使用这种方法的其他线程 - 读取其他文件 - 等待) .

以下是文件被访问和读取的代码片段(逐行)。 这是一个“简化”的示例。正如一些用户在下面评论的那样,这里没有处理异常,而是在真实代码中。这只是为了显示引起麻烦的部分。


// REST CONTROLLER

@GetMapping(value = "/schema/{panel}/{subpanel}")
public PanelSchemaEntity getSchema(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  String fileName = getConfig(panel, subpanel);
  // fileName = "target/config/panelABC1.txt"

  InputStream input = new FileInputStream(fileName);
  PanelSchemaEntity schema = new PanelSchemaEntity();
  parseFile(schema, input);

  return schema;
}

@GetMapping(value = "/data/{panel}/{subpanel}")
public PanelDataEntity get(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  String fileName = getConfig(panel, subpanel);
  // fileName = "target/config/panelABC1.txt"

  InputStream input = new FileInputStream(fileName);
  PanelSchemaEntity schema = new PanelSchemaEntity();
  parseFile(schema, input);

  String dataFileName = getDataFile(panel, subpanel);
  // dataFileName = "target/config/panelABC1.dat"
  InputStream data = new FileInputStream(dataFileName);

  return new PanelDataEntity(schema, data);
}

// PLACED IN SOME UTILS PACKAGE

// Fills the PanelSchemaEntity with the content read from input
public PanelSchemaEntity parseFile(PanelSchemaEntity schema, InputStream input)
{
  BufferedReader reader = new BufferedReader(new InputStreamReader(input));
  String nextLine = reader.readLine();

  // The data file is read and used to complete panel schema entity
  while(nextLine != null)
  {
    // Here goes the code that uses each line's content to 
    // fill some schema's attributes
  }
  reader.close();
  return schema;
}

再次抱歉我可能犯的任何错误,谢谢大家:)

编辑

  • 要多次读取的文件很小,但不能保存在缓存中,因为它与可能很快不会再次打开的面板有关。另外,需要缓存的配置文件太多(每个子面板一个),每个文件都可能更改

  • 我强烈不希望包含新库,因为我没有这样做的权限,我只需要通过在代码中包含小的更改来修复此行为

  • 另外,当调试时,3 个线程(每个线程都打开自己的InputStream 和一个BufferedReader 用于同一个配置文件)完美运行


回答

实际上,InputStreams 可以在不同的线程中创建,都指向同一个文件,然后使用 BufferedReader 读取而不同步任何内容。

我的第一个错误是这里显示了原始代码的精简版本。我专注于展示我认为的问题所在。这是我的第一篇文章,下次我会做得更好。

我的代码中的错误出现在getConfig 方法中,该方法使用面板和子面板参数构建fileName。此方法在返回此 fileName 变量之前,将文件从服务器(仅当它已更改)下载到 target/ 目录以在本地访问。工作错误是文件正在下载总是,因此在当前线程中读取时,它正在被另一个线程重新下载(覆盖)。

在下面找到我原本应该放在帖子中的代码:


// REST CONTROLLER

@GetMapping(value = "/schema/{panel}/{subpanel}")
public PanelSchemaEntity getSchema(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  ConfigFile configFile = getConfig(panel, subpanel);
  // configFile.getPath() = "target/config/panelABC1.txt"

  InputStream input = new FileInputStream(configFile.getPath());
  PanelSchemaEntity schema = new PanelSchemaEntity();
  parseFile(schema, input);

  return schema;
}

@GetMapping(value = "/data/{panel}/{subpanel}")
public PanelDataEntity get(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  ConfigFile configFile = getConfig(panel, subpanel);
  // configFile.getPath() = "target/config/panelABC1.txt"

  InputStream input = new FileInputStream(configFile.getPath());
  PanelSchemaEntity schema = new PanelSchemaEntity();
  parseFile(schema, input);

  String dataFileName = getDataFile(panel, subpanel);
  // dataFileName = "target/config/panelABC1.dat"
  InputStream data = new FileInputStream(dataFileName);

  return new PanelDataEntity(schema, data);
}

// PLACED IN SOME UTILS PACKAGE

// Creates fileName and downloads file (if changed)
public ConfigFile getConfig(String panel, String subpanel)
{
  String filePathInServer = findFilePathInServer(panel, subpanel);

  // ERROR here: the download was happening always
  String localFilePath = donwloadIfChanged(filePathInServer); 

  ConfigFile configFile = new ConfigFile(localFilePath);

  return configFile;
}

// PLACED IN SOME UTILS PACKAGE

// Fills the PanelSchemaEntity with the content read from input
public PanelSchemaEntity parseFile(PanelSchemaEntity schema, InputStream input)
{
  BufferedReader reader = new BufferedReader(new InputStreamReader(input));
  String nextLine = reader.readLine();

  // The data file is read and used to complete panel schema entity
  while(nextLine != null)
  {
    // Here goes the code that uses each line's content to 
    // fill some schema's attributes
  }
  reader.close();
  return schema;
}

当我在 getInputStream 方法中移动 InputStream 创建时,我还在那里包含了文件的下载。这就是为什么同步整个getInputStream = download file + create and return InputStream 对我有用。

这需要在不同的地方修复东西: * 我只需要在文件发生变化时才下载文件(如预期的那样) * 如果文件相同(不使用fileName 字符串),我也会同步整个download + InputStream creation

【问题讨论】:

  • 如果你正在读取的文件很小,你经常从同一个文件中读取,并且你很少更新文件,考虑缓存每个文件中的PanelSchemaEntity。例如,Guava 有一些缓存实用程序,并且应该与并发访问一起使用。只要确保每次更新文件时都使缓存无效。 Guava 的 CacheBuilder,如果你不想自己做缓存:guava.dev/releases/snapshot-jre/api/docs/com/google/common/…
  • 谢谢@simonsays,我认为这是个好主意,但恐怕我不允许使用 Guava 库或任何新库,除非非常必要。实际上,文件很小并且很少更新,但我无法将其存储在缓存中。
  • 如果出现异常,您的代码会泄漏资源。使用 try-with-resources。
  • 嗨,请分享xml文件
  • 这是真正的代码吗?您确定 FileInputStreamBufferedReader 是局部变量吗?不是实例变量或静态变量?

标签: java multithreading concurrency inputstream bufferedreader


【解决方案1】:

对我的情况有效的方法如下:

//REST CONTROLLER
@GetMapping(value = "/schema/{panel}/{subpanel}")
public PanelSchemaEntity getSchema(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  String fileName = getConfig(panel, subpanel);
  // fileName = "target/config/panelABC1.txt"

  InputStream input = getInputStream(fileName);
  PanelSchemaEntity schema = new PanelSchemaEntity();
  return parseFile(schema, input);
}

@GetMapping(value = "/data/{panel}/{subpanel}")
public PanelDataEntity get(String panel, String subpanel)
{
  //Retrieves the config-file name associated to the given panel+subpanel
  String fileName = getConfig(panel, subpanel);
  // fileName = "target/config/panelABC1.txt"

  InputStream input = getInputStream(fileName);
  PanelSchemaEntity schema = new PanelSchemaEntity();
  parseFile(schema, input);

  String dataFileName = getDataFile(panel, subpanel);
  // dataFileName = "target/config/panelABC1.dat"
  InputStream data = new FileInputStream(dataFileName);
  return new PanelDataEntity(schema, data);
}
// PLACED IN SOME UTILS PACKAGE

public InputStream getInputStream(String file)
{
  synchronize (file)
  {
    InputStream input = new FileInputStream(fileName);
  }
}

// Fills the PanelSchemaEntity with the content read from input
public PanelSchemaEntity parseFile(PanelSchemaEntity schema, InputStream input)
{
  BufferedReader reader = new BufferedReader(new InputStreamReader(input));
  String nextLine = reader.readLine();

  // The data file is read and used to complete panel schema entity
  while(nextLine != null)
  {
    // Here goes the code that uses each line's content to 
    // fill some schema's attributes
  }
  reader.close();
  return schema;
}

将 InputStream 初始化移动到 getInputStream 方法,当文件相同时同步。

请随时进行更正。我很感激他们

【讨论】:

  • 您可以使用 Lock 或 ReentrantLock 代替 synchronized 关键字,因为它为您提供了更大的并发处理灵活性。
  • 好的,我已经阅读了它,并且肯定使用 Lock API 比使用 String 作为同步监视器要好得多。我会实施并发布它
  • getInputStream() 方法毫无意义。它创建一个输入流,然后将其丢弃。这里没有什么可以解决问题的。
  • @user207421 通过打印跟踪,我检查了当我毫无意义的getInputStream 没有同步时,一个/一些文件/文件突然停止被读取,或者根本没有被读取。此外,有时三个请求都可以,并且同一文件的三个读取操作可以工作。同步 InputStream 的创建是唯一对我工作的事情,我并不是说这是最好的方法。我也尝试过同步其他块,但失败了。我将尝试改用 ReentrantLock,我觉得这很有用,但如果有更好的解决方案,我会全力以赴,因为我是来学习的
  • 你没有同步任何东西。文件名字符串是动态的,在每种情况下都是根据请求创建的,因此它不是池化的。甚至没有使用生成的输入流。事实上,它被泄露了。 .还有一些其他的解释。
【解决方案2】:

您的问题与并发无关。看这里是同时读取文件的例子。 在同时写入和读取文件的情况下可能会发生并发。但是对于读取文件,没有数据不一致或并发的问题。

请对以下代码进行必要的更改以运行。

public static void main(String[] args) {
    String fileName = "/home/note.xml";
    FileReadThread frth1 = new FileReadThread(fileName, "ThreadOne");
    FileReadThread frth2 = new FileReadThread(fileName, "ThreadTwo");
    FileReadThread frth3 = new FileReadThread(fileName, "ThreadThree");
    frth1.start();
    frth2.start();
    frth3.start();
}}



private String fileName;
private String threadName;
public FileReadThread(String fileName, String threadName) {
    this.fileName = fileName;
    this.threadName = threadName;
}

@Override
public void run() {
    InputStream input;
    try {
        input = new FileInputStream(fileName);
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        String strCurrentLine;
        while ((strCurrentLine = reader.readLine()) != null) {
            System.out.println(threadName + "--" + strCurrentLine);
        }
        reader.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}}

【讨论】:

  • 谢谢!我已经尝试过您提出的建议,但是每个线程在下一个线程开始之前打印整个文件,因此不会同时读取输入文件。这与它已经实现的非常相似,除了我的线程是作为对前端请求的反应而创建的
  • 请多次运行该程序,以便您获得不同的输出,其中包含任意线程响应意味着它们正在并行运行
  • 我的错,我是直接执行每个线程的run 方法而不是启动它们。实际上,在这种情况下,输出会根据需要混合打印。但是在现有代码中包含这个问题仍然存在。这仍然会导致 BufferedReader.readLine() 返回 null,有时在文件中间,有时从头开始(什么都不读)。显然,同步InputStream 的创建解决了我的问题。我已将其包含在新答案中,并将感谢任何评论。无论如何,感谢您的努力和如此说教
  • 这里没有任何东西可以证明你的第一句话,这是最不可能的,或者回答了问题。
猜你喜欢
  • 2019-01-05
  • 2011-04-01
  • 2015-03-24
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
  • 2010-11-28
相关资源
最近更新 更多