В параллельном API поддерживается средство, называемое исполнителем и предназначенное для создания потоков исполнения и управления ими.
В этом отношении исполнитель служит альтернативой управлению потоками исполнения средствами класса Thread.
В основу исполнителя положен интерфейс Executor, в котором определяется следующий метод:
1 |
void execute(Runnable поток) |
В результате вызова этого метода исполняется указанный поток. Следовательно, метод execute() запускает указанный поток на исполнение.
Интерфейс ExecutorService расширяет интерфейс Executor, дополняя его методами, помогающими управлять исполнением потоков и контролировать их.
Например, в интерфейсе ExecutorService определяется метод shutdown(), общая форма которого приведена ниже. Этот метод останавливает все потоки исполнения, находящиеся в данный момент под управлением экземпляра интерфейса ExecutorService.
1 |
void shutdown() |
В интерфейсе ExecutorService определяются также методы, которые запускают потоки исполнения, возвращающие результаты, исполняют ряд потоков и определяют состояние остановки.
Некоторые из этих методов будут рассмотрены далее. Имеется также интерфейс ScheduledExecutorService, расширяющий интерфейс ExecutorService для поддержки планирования потоков исполнения.
Кроме того, в параллельном API имеются три предопределенных класса исполнителей: ThreadPoolExecutor, ScheduledThreadPoolExecutor и ForkJoinPool.
ThreadPoolExecutor реализует интерфейсы Executor и ExecutorService и обеспечивает поддержку управляемого пула потоков исполнения.
Класс ScheduledThreadPoolExecutor также реализует интерфейс ScheduledExecutorService для поддержки планирования пула потоков исполнения.
А класс ForkJoinPool реализует интерфейсы Executor и ExecutorService и применяется в каркасе Fork/Join Framework.
Executor очень важный интерфейс который используется в многопоточном программировании. Если вы не осилили статью, советую посмотреть видео который я добавил в конце статьи. Запасайтесь хорошими наушниками, у меня сейчас Monster Beats купил их здесь, по ценам все хорошо.
Пул потоков предоставляет ряд потоков исполнения для решения разнообразных задач. Вместо того создавать отдельный поток исполнения для каждой задачи, используются потоки из пула.
Это позволяет сократить нагрузку, связанную с созданием множества отдельных потоков. Хотя классы ThreadPoolExecutor и Scheduled ThreadPoolExecutor можно использовать напрямую, исполнитель чаще всего придется получать, вызывая один из следующих статических фабричных методов, определенных во вспомогательном классе Executors. Ниже приведены общие формы некоторых из этих методов.
1 2 3 |
static ExecutorService newCachedThreadPool() static ExecutorService newFixedThreadPool(int количество_потоков) static ScheduledExecutorService newScheduledThreadPool(int количество_потоков) |
Метод newCachedThreadPool() создает пул потоков исполнения, который не только вводит потоки исполнения по мере необходимости, но и по возможности повторно использует их.
Метод newFixedThreadPool() создает пул потоков исполнения, состоящий из указанного количества_потоков. А метод newScheduledThreadPool() создает пул потоков исполнения, в котором можно осуществлять планирование потоков исполнения.
Каждый из них возвращает ссылку на интерфейс ExecutorService, предназначенный для управления пулом потоков исполнения.
В приведенной ниже программе создается фиксированный пул, содержащий два потока исполнения. Затем этот пул используется для выполнения четырех задач. Таким образом, четыре задачи совместно используют два потока исполнения, находящихся в пуле. После того как задачи будут выполнены , пул закрывается и программа завершается.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// Простой пример применения исполнителя import java.util.concurrent.*; class SimpleExec { public static void main(String args[]) { CountDownLatch cd1 = new CountDownLatch(5); CountDownLatch cd2 = new CountDownLatch(5); CountDownLatch cd3 = new CountDownLatch(5); CountDownLatch cd4 = new CountDownLatch(5); ExecutorService es = Executors.newFixedThreadPool(2); System.out.println("Запуск потоков"); // запустить потоки исполнения es.execute(new MyThread(cd1, "A")); es.execute(new MyThread(cd2, "B")); es.execute(new MyThread(cd3, "C")); es.execute(new MyThread(cd4, "D")); try { cd1.await(); cd2.await(); cd3.await(); cd4.await(); } catch(InterruptedException exc) { System.out.println(exc); } es.shutdown(); System.out.println("Завершение потоков"); } } class MyThread implements Runnable { String name; CountDownLatch latch; MyThread(CountDownLatch c, String n) { latch = c; name = n; new Thread(this); } public void run() { for(int i = 0; i < 5; i++) { System.out.println(name + ": " + i); latch.countDown(); } } } |
Ниже приведен результат выполнения данной программы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Запуск потоков A: 0 A: 1 A: 2 B: 0 A: 3 B: 1 A: 4 B: 2 C: 0 B: 3 C: 1 B: 4 C: 2 D: 0 C: 3 D: 1 C: 4 D: 2 D: 3 D: 4 Завершение потоков |
Судя по приведенным выше результатам, выполняются все четыре задачи, несмотря на то, что в пуле содержится всего два потока исполнения. Но только две задачи могут быть выполнены одновременно, а остальные должны ожидать до тех пор, пока в пуле не освободится один из потоков исполнения, чтобы им можно было воспользоваться.
Вызов метода shutdown() очень важен. Если бы его не было в данной программе, она не смогла бы завершиться, поскольку исполнитель оставался бы активным. Убедиться в этом можно, закомментировав вызов метода shutdown ( ) и посмотрев, что из этого получится.