Брюс Эккель - Философия Java3
#4(5). #0(4)
#1(2). #2(2)
#1(Liftoff") */// _ #2(9). #3(9) #4(7). #0(6) #1(4). #2(4) #3(2). #4(2) #2(Liftoff!) #4(9). #0(8) #1(6). #2(6) #3(4). #4(4) #0(1). #1(1) #3(Liftoff!) #1(8). #2(8) #3(6). #4(6) #0(3). #1(3) #2(1). #3(1) #4(Liftoff!)
#3(8). #4(8). #0(7). #1(7).
#0(5). #1(5). #2(5). #3(5).
#2(3). #3(3). #4(3). #0(2).
#4(1). #0(Liftoff1).
Вызов метода sleep() может привести к исключению InterruptedException; перехват этого исключения продемонстрирован в run(). Поскольку исключения не распространяются по потокам обратно в main(), вы должны локально обработать любые исключения, возникающие внутри задачи.
В Java SE5 появилась новая версия sleep(), оформленная в виде метода класса Timellnit; она продемонстрирована в приведенном примере. Она делает программу более наглядной, поскольку вы можете указать единицы измерения продолжительности задержки. Класс Timellnit также может использоваться для выполнения преобразований, как будет показано далее в этой главе.
На некоторых платформах задачи выполняются в порядке «идеального распределения» — от 0 до 4, затем снова от 4 до 0. Это вполне логично, поскольку после каждой команды вывода задача переходит в состояние ожидания, что позволяет планировщику потоков переключиться на другой поток. Тем не менее такое поведение зависит от базовой реализации потокового механизма, поэтому полагаться на него нельзя. Если вам потребуется управлять порядком выполнения задач, используйте средства синхронизации (см. далее) или же вообще откажитесь от использования потоков и напишите собственные функции синхронизации, которые передают управление друг другу в нужном порядке.
Приоритет
Приоритет (priority) потока сообщает планировщику информацию об относительной важности потока. Хотя порядок обращения процессора к существующему набору потоков и не детерминирован, если существует несколько приостановленных потоков, одновременно ожидающих запуска, планировщик сначала запустит поток с большим приоритетом. Впрочем, это не значит, что потоки с младшими приоритетами не выполняются вовсе (то есть тупиковых ситуаций из-за приоритетов не возникает). Потоки с более низкими приоритетами просто запускаются чуть реже.
В подавляющем большинстве случаев все потоки должны выполняться со стандартным приоритетом. Любые попытки манипуляций с приоритетами обычно являются ошибкой.
Следующий пример демонстрирует использование приоритетов. Приоритет существующего потока читается методом getPriority() и задается методом setPriority():
//• concurrency/Si mplePri ori ti es.java
// Использование приоритетов потоков.
import java.util.concurrent.*.
public class SimplePriorities implements Runnable { private int countDown = 5;
private volatile double d; // Без оптимизации продолжение &
private int priority; public SimplePriorities(int priority) { this.priority = priority;
}
public String toStringO {
return Thread.currentThreadО + "; " + countDown;
}
public void runО {
Thread.currentThreadO.setPriority(priority); while(true) {
// Высокозатратная, прерываемая операция; for(int 1=1: i < 100000; i++) {
d += (Math.PI + Math.E) / (double)i; ifCi % 1000 == 0)
Thread.yieldO;
}
System.out.printin(this); if(--countDown == 0) return;
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPoolО; for(int i = 0; i < 5; i++) exec.execute(
new SimplePriorities(Thread.MIN_PRIORITY));
exec.execute(
new SimplePriorities(Thread.MAX_PRIORITY)); exec.shutdownO;
}
} /* Output:
Thread[pool-l-thread-6.10.main]: 5 ThreadEpool-l-thread-6.10.main]: 4 ThreadEpool-l-thread-6.10.main]: 3 ThreadEpool-l-thread-6.10.main]: 2 ThreadEpool-l-thread-6.10.main]: 1 ThreadEpool-l-thread-3.1.main]: 5 ThreadEpool-l-thread-2.1.main]: 5 ThreadEpool-1-thread-l.1.main]: 5 ThreadEpool-l-thread-5.1.main]: 5 ThreadEpool-l-thread-4.1.main]: 5
*///:-
В этой версии метод toStringO переопределяется и использует метод Thread. toString(), который выводит имя потока (его можно задать в конструкторе, но здесь имена автоматически генерируются в виде pool-1-thread-l, pool-l-thread-2 и т. д.), приоритет и группу, к которой принадлежит поток. Переопределенная версия toString() также выводит обратный отсчет, выполняемый задачей. Обратите внимание: для получения ссылки на объект Thread, управляющий задачей, внутри самой задачи, следует вызвать метод Thread.currentThreadO.
Мы видим, что приоритет последнего потока имеет наивысший уровень, а все остальные потоки находятся на низшем уровне. Учтите, что приоритет задается в начале выполнения run(); задавать его в конструкторе бессмысленно, потому что Executor в этот момент еще не начал выполнять задачу.
В метод run() были добавлены 100 ООО достаточно затратных операций с плавающей запятой, включая суммирование и деление с числом двойной точности double. Переменная d была отмечена как volatile, чтобы компилятор не применял оптимизацию. Без этих вычислений вы не увидите эффекта установки различных приоритетов (попробуйте закомментировать цикл for с вычислениями). В процессе вычислений мы видим, что планировщик уделяет больше внимания потоку с приоритетом MAX_PRI0RITY (по крайней мере, таково было поведение программы на машине под управлением Windows ХР). Несмотря даже на то, что вывод на консоль также является «дорогостоящей» операцией, с ним вы не увидите влияние уровней приоритетов, поскольку вывод на консоль не прерывается (иначе экран был бы заполнен несуразицей), в то время как математические вычисления, приведенные выше, прерывать допустимо. Вычисления выполняются достаточно долго, соответственно, механизм планирования потоков вмешивается в процесс и чередует потоки, проявляя при этом внимание к более приоритетным. Тем не менее для обеспечения переключения контекста в программе периодически выполняются команды yield().
В пакете JDK предусмотрено 10 уровней приоритетов, однако это не слишком хорошо согласуется с большинством операционных систем. К примеру, в Windows имеется 7 классов приоритетов, таким образом, их соотношение неочевидно (хотя в операционной системе Sun Solaris имеется 231 уровней). Переносимость обеспечивается толх^о использованием универсальных констант МАХ_РRIORITY, NORM.PRIORITY и MIN_PRI0RITY.
Передача управления
Если вы знаете, что в текущей итерации run() сделано все необходимое, вы можете подсказать механизму планирования потоков, что процессором теперь может воспользоваться другой поток. Эта подсказка (не более чем рекомендация; нет никакой гарантии, что планировщик потоков «прислушается» к ней) воплощается в форме вызова метода yield(). Вызывая yield(), вы сообщаете системе, что в ней могут выполняться другие потоки того же приоритета.
В примере LiftOff метод yield() обеспечивает равномерное распределение вычислительных ресурсов между задачами LiftOff. Попробуйте закомментировать вызов Thread.yield() в Lift0ff.run() и проследите за различиями. И все же, в общем случае не стоит полагаться на yield() как на серьезное средство настройки вашего приложения.
Потоки-демоны
Демоном называется поток, предоставляющий некоторый сервис, работая в фоновом режиме во время выполнения программы, но при этом не является ее неотъемлемой частью. Таким образом, когда все потоки не-демоны заканчивают свою деятельность, программа завершается. И наоборот, если существуют работающие потоки не-демоны, программа продолжает выполнение. Существует, например, поток не-демон, выполняющий метод main().
//: concurrency/SimpleDaemons.java
// Потоки-демоны не препятствуют завершению работы программы
import java.util.concurrent.*.
import static net mindview.util.Print.*;
public class SimpleDaemons implements Runnable { public void run() { try {
while(true) {
TimeUni t.MILLISECONDS.sieep(100). print(Thread.currentThread() + " H + this);
}
} catch(InterruptedException e) {
printC'sleepO interrupted").
}
}
public static void main(String[] args) throws Exception { for(int i = 0. i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemonsO).
daemon setDaemon(true); // Необходимо вызвать перед startO
daemon. startO;
}
printCBce демоны запущены"). TimeUnit.MILLISECONDS sleep(175);
}
} /* Output: Все демоны запущены
Thread[Thread-0.5.main] [email protected] Thread[Thread-1.5.main] [email protected] Thread[Thread-2.5.main] [email protected] Thread[Thread-3,5,main] [email protected] Thread[Thread-4.5.main] [email protected] Thread[Thread-5.5.main] [email protected] Thread[Thread-6.5.main] [email protected] Thread[Thread-7.5.main] [email protected] Thread[Thread-8.5.main] [email protected] Thread[Thread-9.5.main] [email protected]
*///:-
Чтобы назначить поток демоном, следует перед его запуском вызвать метод setDaemon().
После того как main() завершит свою работу, ничто не препятствует завершению программы, поскольку в процессе не работают другие потоки, кроме демонов. Чтобы результаты запуска всех потоков-демонов были более наглядными, поток main() на некоторое время погружается в «сон». Без этого вы увидели бы только часть результатов при создании демонов. (Поэкспериментируйте с вызовом sleep() для интервалов разной продолжительности.)
В примере SimpleDaemons.java используется явное создание объектов Thread для установки их «демонского» флага. Вы также можете настроить атрибуты (демон, приоритет, имя) потоков, созданных исполнителем; для этого следует написать пользовательскую реализацию ThreadFactory:
//: net/mi ndvi ew/uti1/DaemonThreadFactory.java package net.mindview.util;
import java util.concurrent.*;
public class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread t = new Thread(r), t.setDaemon(true); return t,
}
} /// -
Единственное отличие от обычной реализации ThreadFactory заключается в том, что в данном случае атрибут демона задается равным true. Теперь новый объект DaemonThreadFactory передается в аргументе Executors.newCachedThread-Pool():