Java параллельный поток не работает должным образом - PullRequest
0 голосов
/ 26 февраля 2020

У меня есть следующий код:

Map<String, Person> targetPerson = targetPersonList
                                     .stream()
                                     .collect(toMap(Person::getKey,  Function.identity()));

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

Map<String, Person> targetPerson = targetPersonList
                                     .parallelStream()
                                     .collect(toMap(Person::getKey, Function.identity()));

Это на самом деле наоборот, «параллельный» фрагмент занимает 1 час 20 минут. У меня Core i7 8-го поколения, в котором должно быть 6 ядер и 12 потоков, в чем тогда проблема? Что-то не так с моим пониманием параллельных потоков?

Ответы [ 2 ]

4 голосов
/ 26 февраля 2020

Требуется 38 минут только для того, чтобы заполнить HashMap - необычное долгое время. Предполагается, что либо Person::getKey выполняет дорогостоящее построение, либо в результате получается объект с менее чем оптимальной hashCode или equals реализацией.

На моем компьютере заполнение карты десятью миллионами элементов при разумной реализации hashCode или equals требуется меньше секунды, сотням миллионам все еще требуется всего несколько секунд, а затем потребление памяти становится проблемой.

Тем не менее, хуже производительность параллельной поток не приходит неожиданно. Как обсуждалось в « Следует ли мне всегда использовать параллельный поток, когда это возможно? », параллельная обработка имеет некоторые фиксированные издержки, и вам требуется некоторая значительная (на элемент) рабочая нагрузка, чтобы получить преимущество, превышающее накладные расходы.

В вашем конкретном примере c нет никакой выгоды.

Параллельная операция collect работает путем разбиения элементов потока на куски, которые будут обрабатываться различными рабочими потоками. Каждый из них создаст новый локальный контейнер, в случае toMap карта того же типа, что и конечный результат, затем каждый поток будет накапливать элементы в своем локальном контейнере, то есть помещать значения в карту, а когда два рабочих потоки закончили свою работу, частичные результаты будут объединены, что означает помещение всех элементов одной карты в другую.

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

Ваш пример также не включает потенциально дорогостоящие промежуточные операции, поэтому, только если Person::getKey стоит дорого, его стоимость может быть уменьшена параллельной обработкой.

Как обсуждено в , этот ответ , используя toConcurrentMap instea d из toMap может улучшить такой сценарий, поскольку он позволяет пропустить операцию объединения, а наличие полностью уникальных ключей подразумевает очень низкую конкуренцию, когда все рабочие потоки помещаются в одну карту.

Однако, стоит изучить фактическая причина проблемы с производительностью. Когда проблема заключается в реализации ключевого объекта hashCode или equals, ее исправление принесет гораздо больше пользы. Кроме того, параллелизм не может решить проблемы, связанные с почти полной кучей.

Наконец, toConcurrentMap возвращает параллельную карту, что может привести к более высоким затратам на последующую обработку, даже если вы не собираетесь использовать это карта с несколькими потоками.

0 голосов
/ 26 февраля 2020

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

Однако у меня есть несколько советов.

  1. Поскольку вы, вероятно, хотите чтобы использовать HashMap, убедитесь, что вы внедрили и кэшировали результат функции hashCode().
  2. Инициализируйте вашу карту с помощью конструктора, передавая начальную емкость new HashMap<>(targetPersonList.size());
  3. Используйте для -l oop и вставьте каждый элемент, если все значения предварительно рассчитаны.
...