Читаем Многопоточное программирование в Java полностью

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

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

Резюмируя, Threadpool состоит из потоков, которые ищут задания для выполнения.

Вместо запуска нового потока с объектом Runnable, рабочий поток пула потоков просто вызывает функцию run объекта Runnable.

Таким образом, поток в ThreadPool не создается с помощью Runnable, который вы предоставляете, но существующий поток пула потоков просто проверяет, готовы ли какие-либо задачи к выполнению и вызывает их напрямую.

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

Рабочие потоки опрашивают очередь, чтобы увидеть, есть ли задача для выполнения и запускают ее.

Хотя пул потоков является мощным механизмом структурирования многопоточных приложений, он не лишен риска.

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

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

Далее, размер пула потоков должен быть правильно настроен.

Потоки потребляют множество ресурсов, включая память для объекта Thread, стеки выполнения.

Кроме того, JVM, скорее всего, создаст собственный поток для каждого потока Thread, потребляя дополнительные системные ресурсы.

Наконец, есть накладные расходы переключения между потоками.

Если пул потоков слишком велик, ресурсы, потребляемые этими потоками, могут существенно повлиять на производительность системы.

Значительный риск для пулов потоков — это утечка потока, которая возникает, когда поток удаляется из пула для выполнения задачи, но не возвращается в пул, когда задача завершается.

Это может произойти, если задача выбросит исключение.

Если объект пула потока не перехватит исключение и восполнит поток, размер пула потоков будет уменьшен на единицу.

Если это будет происходить постоянно, пул потоков в конечном итоге будет пустым, и система остановится, потому что не будет доступных потоков для обработки задач.

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

Если поток постоянно потребляется такой задачей, он эффективно удаляется из пула.

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

Для эффективного использования пула потоков, не ставьте в очередь задачи, которые ждут синхронно результатов других задач.

Это может вызвать deadlock, когда все потоки заняты задачами, которые в свою очередь ожидают результатов от задач, поставленных в очередь, которые не могут выполняться, потому что все потоки заняты.

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

Если у вас разные типы задач с отличающимися характеристиками, имеет смысл создать несколько пулов с соответствующими характеристиками.

Для чисто вычислительных задач имеет смысл определить размер пула равным количеству процессоров системы.

Для сетевых задач размер пула можно оценить, как N* (1+WT/ST) (N-количество процессоров, WT-время ожидания ресурса, ST-время обслуживания запроса).

В этом примере создается пул из одного потока и неограниченной очереди.

И если это многопользовательское приложение, тогда при каждом запросе, создающем объект Runnable, этот код будет помещать новый объект Runnable в очередь на выполнение единственным потоком из пула.

Здесь пул потока создается отдельно при запуске приложения.

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

Можно вызвать метод shutdownNow, который прерывает все выполняемые задачи и немедленно завершает работу executor.

Хороший способ закрыть ExecutorService, это использовать оба этих методов в сочетании с методом awaitTermination.

Перейти на страницу:

Похожие книги