Java: чтение одного и того же файла из разных потоков иногда возвращает нулевое содержимое в некоторых потоках - PullRequest
2 голосов
/ 06 октября 2019

Прежде всего, я довольно новичок в кодировании, поэтому я прошу прощения за любую ошибку argot, которую я, вероятно, совершаю

Я работаю над разработкой внутреннего сервера с использованием Java (openJDK11) и Spring boot :

Приложение состоит из множества панелей и подпанелей, которые открываются из веб-браузера. При щелчке на дополнительной панели три разных запроса GET выполняются на внешней стороне.

Эти три запроса ожидают разных ответов (схема json, данные json и т. Д.). Каждый запрос запускает отдельный поток, который обращается к к одному и тому же файлу конфигурации (существует один файл конфигурации для каждой подпанели), который анализируется. После прочтения файла конфигурации каждый поток выполняет различные операции, у них есть только общая часть для чтения конфигурации.

  • Иногда (это заставляет меня думать о параллелизме) операция чтения водин / некоторые из этих потоков не могут быть выполнены, так как 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;
}

Когда я переместил создание InputStream в метод getInputStream, я также включил туда загрузкуфайл. Вот почему синхронизация всего getInputStream = download file + create and return InputStream сработала для меня.

Это требует исправления в разных местах: * Мне нужно скачивать файл только тогда, когда он изменился (как и ожидалось) * Я также синхронизировал бы весь download + InputStream creation для случая, когда файл одинаков (не используется строка fileName)

Ответы [ 2 ]

1 голос
/ 07 октября 2019

Ваша проблема не связана с параллелизмом. Смотрите здесь пример чтения файлов одновременно. Параллельность может возникнуть в случае записи и чтения файлов одновременно. Но для чтения файлов не возникает проблем с несогласованностью или параллелизмом данных.

Pleae внесите необходимые изменения в следующий код для запуска.

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();
    }
}}
0 голосов
/ 07 октября 2019

В моем случае сработало следующее:

//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, который синхронизируется, когда файлы совпадают.

Пожалуйста, не стесняйтесь вносить исправления. Я ценю их

...