Проблема со вставкой строки в StringBuffer/StringBuilder с потоком/parallelStream

Я пытался вставить строки в StringBuffer, используя метод foreach для parallelStream(), созданный из коллекции Set. Проблема в том, что каждый раз, когда я выполняю код, конечная строка (StringBuffer.toString()) имеет на 1 элемент меньше общего (случайный элемент каждый раз, когда я пытаюсь).

Я также меняю StringBuffer на StringBuilder, parallelStream() на stream(), но всегда на 1 элемент меньше.

Я использую: - Версия Java: java 1.8_121 - Сервер: Weblogic 12.2.1.2 (я не думаю, что это имеет отношение к проблеме) - Spring boot 2.0.2.RELEASE (я не думаю, что это имеет отношение к проблема)

ПРИМЕЧАНИЕ. Я использовал карту, чтобы сохранить PDF-файлы, которые я должен подписать позже в процессе (в другом HTTP-запросе).

Map<String, ClientPdf> dataToEncript = new HashMap<>(); // pdf name it will be the key for this map (it is unique in the sql query)

List<Client> listClients = // list of clients from database
Set<ClientPdf> clientsPdf = new HashSet<>();
for (Client client : listClients) { 
    clientsPdf.add(client.clientPdf()); // clientPdf() generate a new object ClientPdf, which is similar to Client class, but with less fields (essential for the Set)
}

log.debug("Generating documents");
clientsPdf.parallelStream().forEach(pdf -> {
    // some code to generate pdf

    log.debug("Inserting pdf: {}", pdf); // this log print, for example, 27.000 lines
    dataToEncript.put(pdf.getPdfName(), pdf);
});


StringBuffer sb = new StringBuffer(); // StringBuffer or StringBuilder, the same problem
for (ClientPdf clientPdf : dataToEncript.values()) {
    sb.append(clientPdf.getPdfName() + ";" + clientPdf.getRut() + "\n"); // appending all values of de map dataToEncript, it will append 26.669 (1 less)
}

person juliovr    schedule 30.05.2019    source источник
comment
Существует ли одно и то же значение pdfName, но другое значение рута в вашем наборе ClientPdf?   -  person LHCHIN    schedule 31.05.2019
comment
Нет. В моей стране (Чили) рут является уникальным идентификатором людей, поэтому у клиента может быть более 1 pdf, но pdf принадлежит только 1 человеку.   -  person juliovr    schedule 31.05.2019
comment
Хорошо, или вы могли бы распечатать размер набора cliendsPdf, прежде чем объединять значения с помощью StringBuffer или StringBuilder?   -  person LHCHIN    schedule 03.06.2019
comment
В clientesPdf.parallelStream().forEach() я распечатываю все имена pdf и, например, 27 000 pdf; но когда я добавляю имена в StringBuffer (или StringBuilder), добавляется только 26,999 (на 1 меньше). Самое странное, что пропущенный pdf-файл отличается для каждого исполнения. Я думаю, что проблема в редко странной ситуации связана с парой stream()-StringBuffer (или builder).   -  person juliovr    schedule 03.06.2019


Ответы (2)


clientsPdf.parallelStream().forEach(pdf -> {
    // ... 
    dataToEncript.put(pdf.getPdfName(), pdf);
});

dataToEncript не является потокобезопасной структурой данных, поэтому это может вызвать нелепые и странные ошибки, подобные той, которую вы наблюдаете.

В общем, использование forEach часто является плохим признаком, и вы почти всегда должны использовать Collector или какой-либо другой метод. Например, здесь вам, вероятно, следует использовать

clientsPdf.parallelStream()
   .collect(Collectors.toConcurrentMap(Pdf::getPdfName, pdf -> pdf));

чтобы получить правильную карту.

Еще лучше, вы могли бы написать

clientsPdf.parallelStream()
    .map(clientPdf -> clientPdf.getPdfName() + ";" + clientPdf.getRut() + "\n")
    .collect(Collectors.joining())

чтобы получить окончательный String без какого-либо ручного управления StringBuffer и т.п.

person Louis Wasserman    schedule 30.05.2019
comment
Большое спасибо, Луис, за ваш ответ, но у меня есть несколько вопросов: - Почему вы говорите, что это плохой знак, используя оператор forEach в этом случае? - Что касается dataToEncript, который не является потокобезопасным, я также изменил parallelStream() на простой stream(), и эта странная ошибка все еще появлялась; Не знаю, встречались ли вы с такой проблемой раньше. - person juliovr; 31.05.2019

Поскольку HashMap не является потокобезопасным, как упоминал Вассерман выше.
Это может привести к несогласованности в состоянии HashMap, если несколько потоков обращаются к одному и тому же объекту и пытаются изменить его структуру.

Таким образом, HashTable, SynchronizedMap или ConcurrentHashMap используются для использования HashMap в многопоточной среде (такой как parallelStream()).

Вы можете просто переписать первую строку кода следующим образом:

Map<String, ClientPdf> dataToEncript = Collections.synchronizedMap(new HashMap<>());

Теперь вы должны получить правильный результат после повторного запуска вашей программы.

Кстати, и HashTable, и SynchronizedMap не очень хороши в производительности, вместо этого вы можете использовать ConcurrentHashMap, чтобы решить эту проблему.

Удачи!

person LHCHIN    schedule 04.06.2019