FixedThreadPool недостаточно параллелен - PullRequest
3 голосов
/ 21 марта 2012

Я создаю фиксированный пул потоков, используя forPool = Executors.newFixedThreadPool(poolSize);, где poolSize инициализируется числом ядер на процессоре (скажем, 4).В некоторых случаях он работает нормально, и загрузка ЦП постоянно составляет 400%.

Но иногда использование снижается до 100% и никогда не повышается до 400%.У меня запланировано 1000 задач, так что проблема не в этом.Я ловлю каждое исключение, но исключение не выбрасывается.Таким образом, проблема является случайной и не воспроизводимой, но очень существенной.Это данные параллельных операций.В конце каждого потока есть синхронизированный доступ для обновления одной переменной.Маловероятно, что у меня там тупик.Фактически, если я обнаружу эту проблему, если я уничтожу пул и создам новый размер 4, он все равно будет использоваться только на 100%.Ввод-вывод отсутствует.

Кажется, это противоречит интуитивному пониманию Java «FixedThreadPool».Я неправильно прочитал гарантию?Гарантируется ли только параллелизм, а не параллелизм?

И на вопрос - Вы сталкивались с этой проблемой и решили ее?Если я хочу параллелизма, я делаю правильную вещь?

Спасибо!

При выполнении дампа потока: я обнаружил, что 4 потока все выполняют свои параллельные операции.Но использование все еще ~ 100% только.Вот дампы потока при 400% использовании и 100% использовании .Я установил количество потоков на 16, чтобы запустить сценарий.Некоторое время он работает на 400%, а затем падает до 100%.Когда я использую 4 потока, он работает на 400% и редко падает до 100%. Это - код распараллеливания.

****** [ОСНОВНОЕ ОБНОВЛЕНИЕ] ******

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

Ответы [ 8 ]

5 голосов
/ 09 апреля 2012

Учитывая тот факт, что увеличение размера кучи заставляет проблему «уйти» (возможно, не навсегда), проблема, вероятно, связана с GC.

Возможно ли, что реализация Operation генерирует какое-то состояние,что хранится в куче, между вызовами на

pOperation.perform(...);

?Если это так, то у вас может быть проблема с использованием памяти, возможно, утечка.Чем больше задач выполнено, тем больше данных находится в куче.Сборщик мусора должен работать все больше и больше, чтобы попытаться восстановить столько, сколько он может, постепенно занимая 75% от общего объема доступных ресурсов ЦП.Даже уничтожение ThreadPool не поможет, потому что это не то место, где хранятся ссылки, оно в Операции.

Случай с 16-ю потоками, затрагивающий эту проблему чаще, может быть связан с тем, что он генерирует больше состояний быстрее (я не знаю реализацию Операции, мне так сложно это сказать).

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

2 голосов
/ 12 апреля 2012

Мой ответ основан на сочетании знаний об управлении памятью JVM и некоторых предположений о фактах, о которых я не смог найти точную информацию.Я считаю, что ваша проблема связана с буферами локального выделения потока (TLAB), которые использует Java:

Буфер локального выделения потока (TLAB) - это область Eden, которая используется для выделения однимнить.Он позволяет потоку выполнять выделение объектов с помощью локальных указателей top и limit потока, что быстрее, чем выполнение атомарной операции над указателем top, совместно используемым потоками.

Допустим, у вас есть размер eden2M и использовать 4 потока: JVM может выбрать размер TLAB (eden / 64) = 32K, и каждый поток получает TLAB такого размера.Как только 32 КБ TLAB потока исчерпаны, он должен получить новый, который требует глобальной синхронизации.Глобальная синхронизация также необходима для выделения объектов, которые больше, чем TLAB.

Но, честно говоря, все не так просто, как я описал: JVM адаптивно измеряет TLAB потока на основе его предполагаемогоскорость выделения определяется на малых GC [ 1 ], что делает поведение, связанное с TLAB, еще менее предсказуемым.Тем не менее, я могу себе представить, что JVM уменьшает размеры TLAB, когда работает больше потоков.Кажется, это имеет смысл, потому что сумма всех TLAB должна быть меньше доступного пространства Eden (и даже некоторой части пространства Eden на практике, чтобы иметь возможность заполнять TLAB).

Предположим, чтофиксированный размер TLAB на поток (размер eden / (16 * работающих пользовательских потоков)):

  • для 4 потоков, что приводит к TLAB 32K
  • для 16 потоков, что приводит к TLABиз 8K

Вы можете себе представить, что 16 потоков, которые исчерпывают свой TLAB быстрее, поскольку он меньше, вызовут гораздо больше блокировок на распределителе TLAB, чем 4 потока с 32K TLAB.

В заключение,когда вы уменьшаете количество рабочих потоков или увеличиваете объем памяти, доступной для JVM, потокам могут быть предоставлены большие TLAB, и проблема будет решена.

https://blogs.oracle.com/daviddetlefs/entry/tlab_sizing_an_annoying_little

2 голосов
/ 21 марта 2012

Я предлагаю вам использовать функцию Yourkit Thread Analysis , чтобы понять реальное поведение.Он точно скажет вам, какие потоки запущены, заблокированы или ожидают и почему.

Если вы не можете / не хотите приобретать его, следующий лучший вариант - использовать Visual VM , который связан с JDK, чтобы сделать этот анализ.Он не даст вам такой подробной информации, как Yourkit.Следующее сообщение в блоге поможет вам начать работу с Visual VM: http://marxsoftware.blogspot.in/2009/06/thread-analysis-with-visualvm.html

1 голос
/ 12 апреля 2012

Настройка JVM

Ядром платформы Java является виртуальная машина Java (JVM).Весь сервер приложений Java работает внутри JVM.JVM принимает много параметров запуска в качестве флагов командной строки, и некоторые из них имеют большое влияние на производительность приложения.Итак, давайте рассмотрим некоторые важные параметры JVM для серверных приложений.

Во-первых, вы должны выделить как можно больше памяти для JVM, используя флаги -Xms (минимальная память) и -Xmx (максимальная память).Например, тег -Xms1g -Xmx1g выделяет 1 ГБ ОЗУ для JVM.Если вы не укажете размер памяти в флагах запуска JVM, JVM ограничит объем памяти кучи 64 МБ (512 МБ в Linux), независимо от того, сколько физической памяти у вас на сервере!Больше памяти позволяет приложению обрабатывать больше параллельных веб-сеансов и кэшировать больше данных, чтобы улучшить медленный ввод-вывод и операции с базой данных.Обычно мы указываем одинаковый объем памяти для обоих флагов, чтобы заставить сервер использовать всю выделенную память при запуске.Таким образом, JVM не нужно будет динамически изменять размер кучи во время выполнения, что является основной причиной нестабильности JVM.Для 64-битных серверов убедитесь, что вы запускаете 64-битную JVM поверх 64-битной операционной системы, чтобы использовать всю оперативную память на сервере.В противном случае JVM сможет использовать только 2 ГБ или меньше памяти.64-разрядные JVM обычно доступны только для JDK 5.0.

При большой куче памяти операция сбора мусора (GC) может стать основным узким местом производительности.GC может пройти более десяти секунд, чтобы пройти через кучу в несколько гигабайт.В JDK 1.3 и более ранних версиях GC является однопоточной операцией, которая останавливает все другие задачи в JVM.Это не только вызывает длительные и непредсказуемые паузы в приложении, но также приводит к очень низкой производительности на многопроцессорных компьютерах, поскольку все остальные процессоры должны ждать в режиме ожидания, пока один процессор работает на 100%, чтобы освободить пространство кучи памяти.Крайне важно, чтобы мы выбрали JVK JDK 1.4+, который поддерживает параллельные и параллельные операции GC.На самом деле, параллельная реализация GC в JVM серии JDK 1.4 не очень стабильна.Поэтому мы настоятельно рекомендуем вам обновить JDK до версии 5.0.Используя флаги командной строки, вы можете выбрать один из следующих двух алгоритмов GC.Оба они оптимизированы для многопроцессорных компьютеров.

  • Если вашим приоритетом является увеличение общей пропускной способности приложения, и вы можете допускать случайные паузы GC, вам следует использовать -XX: UseParallelGC и -XX: Использовать флаги ParallelOldGC (последний доступен только в JDK 5.0) для включения параллельного GC.Параллельный GC использует все доступные процессоры для выполнения операции GC, и, следовательно, он намного быстрее, чем однопоточный GC по умолчанию.Однако он по-прежнему приостанавливает все другие действия в JVM во время GC.
  • Если вам нужно минимизировать паузу GC, вы можете использовать флаг -XX: + UseConcMarkSweepGC для включения одновременного GC.Параллельный GC по-прежнему приостанавливает JVM и использует параллельный GC для очистки недолговечных объектов.Однако он очищает долгоживущие объекты из кучи, используя фоновый поток, работающий параллельно с другими потоками JVM.Параллельный сборщик мусора значительно уменьшает паузу сборщика мусора, но управление фоновым потоком увеличивает нагрузку на систему и снижает общую пропускную способность.

Кроме того, есть еще несколько параметров JVM, на которые можно настроитьоптимизировать операции GC.

  • В 64-разрядных системах стеку вызовов для каждого потока выделяется 1 МБ пространства памяти.Большинство потоков не используют столько места.Используя флаг -XX: ThreadStackSize = 256 КБ, вы можете уменьшить размер стека до 256 КБ, чтобы разрешить больше потоков.
  • Используйте флаг -XX: + DisableExplicitGC, чтобы игнорировать явные вызовы приложения System.gc ().Если приложение вызывает этот метод часто, то мы могли бы сделать много ненужных сборщиков мусора.
  • Флаг -Xmn позволяет вам вручную установить размер "молодого"«пространство памяти поколения» для недолговечных объектов. Если ваше приложение генерирует много новых объектов, вы можете значительно улучшить ГХ, увеличив это значение. Размер «молодого поколения» почти никогда не должен превышать 50% кучи.

Поскольку GC оказывает большое влияние на производительность, JVM предоставляет несколько флагов, которые помогут вам точно настроить алгоритм GC для вашего конкретного сервера и приложения. В этой статье обсуждаются алгоритмы GC и советы по настройке.подробно, но мы хотели бы отметить, что JVK JDK 5.0 поставляется с функцией адаптивной настройки GC, называемой эргономикой, которая может автоматически оптимизировать параметры алгоритма GC на основе базового оборудования, самого приложения и желаемых целей, определяемыхпользователь (например, максимальное время паузы и желаемая пропускная способность). Это экономит ваше время, когда вы сами пытаетесь использовать различные комбинации параметров GC. Эргономика - еще одна веская причина для перехода на JDK 5.0. Заинтересованные читатели могут обратиться к разделу «Настройка мусора».ollection с виртуальной машиной Java 5.0.Если алгоритм GC настроен неправильно, выявить проблемы на этапе тестирования вашего приложения относительно легко.В следующем разделе мы обсудим несколько способов диагностики проблем GC в JVM.

Наконец, убедитесь, что вы запускаете JVM с флагом -server.Он оптимизирует компилятор Just-In-Time (JIT) для торговли с более медленным временем запуска для более быстрой производительности во время выполнения.Есть еще флаги JVM, которые мы не обсуждали;Подробнее об этом см. на странице документации по опциям JVM.

Ссылка: http://onjava.com/onjava/2006/11/01/scaling-enterprise-java-on-64-bit-multi-core.html

1 голос
/ 12 апреля 2012

Это почти наверняка из-за GC.

Если вы хотите быть уверены, добавьте следующие флаги запуска в вашу программу Java:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps и проверьте стандартный вывод.

Вы увидите строки, содержащие «Full GC», включая время, которое это заняло: за это время вы увидите 100% загрузку ЦП.

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

Так что, вероятно, происходит то, что в вашем примере со 100% процессором GC работает со старым поколением, которое выполняется в одном потоке, и поэтому поддерживает только одно ядро.

Предложение для решения: используйте одновременный сборщик меток и разверток, используя флаг
-XX:+UseConcMarkSweepGC при запуске JVM.

0 голосов
/ 12 апреля 2012

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

Поскольку вы предоставляете JVM ~ 90% физической памяти на машинах, проблема может быть связана с вводом-выводом, возникающим из-за подкачки и перестановки памяти при попытке выделить память для большего количества объектов. Обратите внимание, что физическая память также используется другими работающими процессами, а также ОС. Кроме того, поскольку симптомы появляются через некоторое время, это также указывает на утечки памяти.

Попробуйте выяснить, сколько физической памяти доступно (еще нет используется) и выделите ~ 90% доступной физической памяти для вашей кучи JVM.

  • Что произойдет, если вы оставите систему включенной на продолжительный период время

  • Возвращается ли он когда-либо к загрузке процессора на 400%?

  • Вы замечаете какую-либо активность диска, когда загрузка процессора составляет 100%?
  • Можете ли вы отслеживать, какие потоки работают, а какие заблокированы и когда?

Посмотрите следующую ссылку для настройки: http://java.sun.com/performance/reference/whitepapers/tuning.html#section4

0 голосов
/ 06 апреля 2012

Поскольку вы используете блокировку, возможно, что один из ваших четырех потоков достигает блокировки, но затем переключается в контекст - возможно, для запуска потока GC.Другие потоки не могут прогрессировать, так как они не могут получить блокировку.Когда контекст потока переключается обратно, он завершает работу в критической секции и снимает блокировку, чтобы позволить только одному другому потоку достигнуть блокировки.Итак, теперь у вас есть две активные темы.Возможно, что в то время как второй поток выполняет критическую секцию, первый поток выполняет параллельную работу над следующим фрагментом данных, но генерирует достаточно мусора, чтобы вызвать GC, и мы возвращаемся к тому, с чего начали:)

PS Этопросто лучшее предположение, так как трудно понять, что происходит, без каких-либо фрагментов кода.

0 голосов
/ 21 марта 2012

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

Если у вас высокий уровень ввода-вывода, вы можете получить менее 400%, но маловероятно, что вы получите круглое число использования процессора.Например, вы можете увидеть 38%, 259%, 72%, 9% и т. д. (Это также может привести к скачкам)

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

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

...