#2(7). #2(6). #2(5). #2(4). #2(3). #2(2). #2(1). #2
#3(6), #3(5). #3(4). #3(3). #3(2). #3(1). «(Liftoff!). #4(9). #4(8). #4(7). #4(6).
#4(5). #4(4). #4(3). #4(2). #4(1). #4(Liftoff!).
Другой пример: допустим, имеется группа потоков, выполняющих операции с использованием файловой системы. Вы можете запустить эти задачи под управлением SingleThreadExecutor, чтобы в любой момент гарантированно выполнялось не более одной задачи. При таком подходе вам не придется возиться с синхронизацией доступа к общим ресурсам (без риска для целостности файловой системы). Возможно, в окончательной версии кода будет правильнее синхронизировать доступ к ресурсу (см. далее в этой главе), но SingleThreadExecutor позволит быстро организовать координацию доступа при построении рабочего прототипа.
Возврат значений из задач
Интерфейс Runnable представляет отдельную задачу, которая выполняет некоторую работу, но не возвращает значения. Если вы хотите, чтобы задача возвращала значение, реализуйте интерфейс Callable вместо интерфейса Runnable. Параметризованный интерфейс Callable, появившийся в Java SE5, имеет параметр типа, представляющий возвращаемое значение метода call() (вместо run()), а для его вызова должен использоваться метод ExecutorService submit(). Простой пример:
//. concurrency/Cal1ableDemo.java
import java.util concurrent.*;
import java.util.*,
class TaskWithResult implements Callable
private int id;
public TaskWithResult(int id) { this id = id.
}
public String call О {
return "результат TaskWithResult " + id;
}
}
public class CallableDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPoolО; ArrayList
new ArrayList
results.add(exec submit(new TaskWithResult(i))); for(Future
// Вызов get О блокируется до завершения; System.out.pri nt1n(fs.get()); } catch(InterruptedException e) { System.out.println(e). return;
} catch(ExecutionException e) { System out println(e); } finally {
exec.shutdown();
}
} /* Output•
результат TaskWithResult О результат TaskWithResult 1 результат TaskWithResult 2 результат TaskWithResult 3 результат TaskWithResult 4 результат TaskWithResult 5 результат TaskWithResult 6 результат TaskWithResult 7 результат TaskWithResult 8 результат TaskWithResult 9 *///:-
Метод submit() создает объект Future, параметризованный по типу результата, возвращаемому Callable. Вы можете обратиться к Future с запросом isDone(), чтобы узнать, завершена ли операция. После завершения задачи и появления результата производится его выборка методом get(). Если get() вызывается без предварительной проверки isDone(), вызов блокируется до появления результата. Также можно вызвать get() с интервалом тайм-аута.
Перегруженный метод Executors.callable() получает Runnable и выдает Callable. ExecutorService содержит методы для выполнения коллекций объектов Callable.
Ожидание
Другим способом управления вашими потоками является вызов метода sleep(), который переводит поток в состояние ожидания на заданное количество миллисекунд. Если в классе LiftOff заменить вызов yield() на вызов метода sleep(), будет получен следующий результат:
//: concurrency/SleepingTask.java // Вызов sleepO для приостановки потока, import java.util.concurrent.*;
public class SIeepingTask extends LiftOff { public void run() { try {
while(countDown-- > 0) {
System.out.pri nt(status());
// Старый стиль.
// Thread.sleep(lOO);
// Стиль Java SE5/6:
TimeUnit MILLISECONDS.sieep(100);
}
} catchdnterruptedException e) {
System.err.pri ntin("Interrupted");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPoolО; for(int i = 0; i < 5; i++)
exec.execute(new SIeepi ngTask()); exec.shutdownO;
}
#0(9). #1(9)
#2(7). #3(7)
#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. Это вполне логично, поскольку после каждой команды вывода задача переходит в состояние ожидания, что позволяет планировщику потоков переключиться на другой поток. Тем не менее такое поведение зависит от базовой реализации потокового механизма, поэтому полагаться на него нельзя. Если вам потребуется управлять порядком выполнения задач, используйте средства синхронизации (см. далее) или же вообще откажитесь от использования потоков и напишите собственные функции синхронизации, которые передают управление друг другу в нужном порядке.
Приоритет
В подавляющем большинстве случаев все потоки должны выполняться со стандартным приоритетом. Любые попытки манипуляций с приоритетами обычно являются ошибкой.
Следующий пример демонстрирует использование приоритетов. Приоритет существующего потока читается методом 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() сделано все необходимое, вы можете подсказать механизму планирования потоков, что процессором теперь может воспользоваться другой поток. Эта подсказка
В примере LiftOff метод yield() обеспечивает равномерное распределение вычислительных ресурсов между задачами LiftOff. Попробуйте закомментировать вызов Thread.yield() в Lift0ff.run() и проследите за различиями. И все же, в общем случае не стоит полагаться на yield() как на серьезное средство настройки вашего приложения.
Потоки-демоны
//: 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] SimpleDaemons@530daa Thread[Thread-1.5.main] SimpleDaemons@a62fc3 Thread[Thread-2.5.main] SimpleDaemons@89ae9e Thread[Thread-3,5,main] SimpleDaemons@1270b73 Thread[Thread-4.5.main] SimpleDaemons@60aeb0 Thread[Thread-5.5.main] SimpleDaemons@16caf43 Thread[Thread-6.5.main] SimpleDaemons@66848c Thread[Thread-7.5.main] SimpleDaemons@8813f2 Thread[Thread-8.5.main] SimpleDaemons@ld58aae Thread[Thread-9.5.main] SimpleDaemons@83cc67
*///:-
Чтобы назначить поток демоном, следует перед его запуском вызвать метод 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():
//: concurrency/DaemonFromFactory java
// Использование ThreadFactory для создания демонов.
import java.util.concurrent.*;
import net mindview util *;
import static net.mindview.util.Print.*,
public class DaemonFromFactory implements Runnable { public void run() { try {
while(true) {
TimeUnit MILLISECONDS.sleep(lOO); print(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) { print("Interrupted");
}
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors newCachedThreadPool(
new DaemonThreadFactory()), for(int i = 0; i < 10. i++)
exec.execute(new DaemonFromFactory()); printC'Bce демоны запущены"); TimeUnit MILLISECONDS.sleep(500); // Задержка
}
} /// ~
Каждый статический метод создания ExecutorService перегружается для получения объекта ThreadFactory, который будет использоваться для создания новых потоков.
Сделаем еще один шаг — создадим вспомогательный класс DaemonThread-PoolExecutor:
// net/mi ndvi ew/uti1/DaemonThreadPoolExecutor.java package net mindview.util; import java util.concurrent *;
public class DaemonThreadPoolExecutor extends ThreadPoolExecutor {
public DaemonThreadPoolExecutorО {
super(0, Integer MAX_VALUE. 60L. TimeUnit SECONDS.
new SynchronousQueue
new DaemonThreadFactoryO).
}
} /// ~
Чтобы узнать, какие значения должны передаваться при вызове конструктора базового класса, я просто заглянул в исходный код Executors.java.
Чтобы узнать, является ли поток демоном, вызовите метод isDaemon(). Если поток является демоном, то все потоки, которые он производит, также будут демонами, что и демонстрируется следующим примером:
//: concurrency/Daemons.java
// Потоки, порождаемые демонами, также являются демонами
import java util.concurrent.*,
import static net mindview util Print.*,
class Daemon implements Runnable {
private Thread[] t = new Thread[10]; public void run() {
for(int i = 0; i < t length; i++) {
t[i] = new Thread (new DaemonSpawnO); t[i].startO:
printnb("DaemonSpawn " + i + " started. ");
}
for(int i = 0. i < t.length, i++)
printnb("t[" + i + "]. isDaemonO = " + t[i] isDaemonO + ");
while(true)
Thread.yieldO;
class DaemonSpawn implements Runnable { public void run() { while(true)
Thread.yieldO;
public class Daemons {
public static void main(String[] args) throws Exception { Thread d = new Thread(new DaemonO); d.setDaemon(true); d.startO;
printnbC'd.isDaemonO = " + d.isDaemonO + ". "); // Даем потокам-демонам завершить процесс запуска: TimeUnit.SECONDS.sleep(l);
}
} /* Output:
d.isDaemonO = true, DaemonSpawn 0 started, DaemonSpawn 1 started. DaemonSpawn 2 started. DaemonSpawn 3 started. DaemonSpawn 4 started. DaemonSpawn 5 started. DaemonSpawn 6 started, DaemonSpawn 7 started, DaemonSpawn 8 started. DaemonSpawn 9 started. t[0].isDaemonO = true. t[l] isDaemonO = true, t[2].isDaemonO = true, t[3].isDaemonO = true. t[4].isDaemonO = true. t[5].isDaemonO = true. t[6].isDaemonO = true. t[7].isDaemonO = true. t[8].isDaemonO = true. t[9].isDaemonO = true. *///:-
Поток Daemon переводится в режим демона, а затем порождает группу новых потоков, которые
Учтите, что потоки-демоны завершают свои методы run() без выполнения секций finally:
//: concurrency/DaemonsDontRunFinally.java
// Потоки-демоны не выполняют секцию finally.
import java.util.concurrent.*.
import static net.mindview.util.Print.*,
class ADaemon implements Runnable { public void run() { try {
print("Запускаем ADaemon"); TimeUnit.SECONDS.sieep(l). } catch(InterruptedException e) {
print("Выход через InterruptedException"); } finally {
print("Должно выполняться всегда?");
}
}
}
public class DaemonsDontRunFinally {
public static void main(String[] args) throws Exception { Thread t = new Thread(new ADaemonO); t.setDaemon(true). t.startO,
}
} /* Output;
Запускаем ADaemon
*///:-
Запуск программы наглядно показывает, что секция finally не выполняется. С другой стороны, если закомментировать вызов setDaemon(), вы увидите, что секция finally была выполнена.
Такое поведение верно, даже если из предыдущих описаний finally у вас сложилось обратное впечатление. Демоны завершаются «внезапно», при завершении последнего не-демона. Таким образом, сразу же при выходе из main() JVM немедленно прерывает работу всех демонов, не соблюдая никакие формальности. Невозможность корректного завершения демонов ограничивает возможности их применения. Обычно объекты Executor оказываются более удачным решением, потому что все задачи, находящиеся под управлением Executor, могут быть завершены одновременно.
Варианты кодирования
Во всех предшествующих примерах все классы задач реализовали интерфейс Runnable. В очень простых случаях можно использовать альтернативное решение с прямым наследованием от Thread:
//• concurrency/SimpleThread.java // Прямое наследование от класса Thread.
public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0. public SimpleThreadO {
// Сохранение имени потока
super(Integer.toStri ng(++threadCount)).
startO.
}
public String toStringO {
return "#" + getNameO + "(" + countDown + "), ";
}
public void run() {
while(true) {
System out print(this). if(--countDown == 0) return,
}
}
public static void main(String[] args) { for(int i = 0, i < 5, i++) new SimpleThreadO.
}
} /* Output
#1(5). #1(4). #1(3). #1(2). #1(1). #2(5). #2(4). #2(3). #2(2). #2(1). #3(5). #3(4). #3(3). #3(2). #3(1). #4(5). #4(4), #4(3). #4(2). #4(1). #5(5). #5(4). #5(3). #5(2). #5(1).
Чтобы задать объектам Thread имена, вы вызываете соответствующий конструктор Thread. Имя читается в методе toStringO при помощи getName().
Также иногда встречается идиома самоуправляемой реализации Runnable:
// concurrency/SelfManaged.java
// Реализация Runnable. содержащая собственый объект Thread
public class SelfManaged implements Runnable { private int countDown = 5. private Thread t = new Thread(this). public SelfManagedO { t startO. } public String toStringO {
return Thread currentThreadO .getNameO + "(" + countDown + "). ";
}
public void run() {
while(true) {
System out print(this). if(--countDown == 0) return.
}
}
public static void main(Stnng[] args) { for(int i = 0. i < 5. i++) new SelfManagedO,
}
Thread-0(5) Thread-1(4) Thread-2(3) Thread-3(2) Thread-4(1) */// ~
В целом происходящее не так уж сильно отличается от наследования от Thread, разве что синтаксис получается чуть более громоздким. Однако реализация интерфейса позволяет наследовать от другого класса, тогда как в варианте с Thread это невозможно.
Обратите внимание на вызов start() в конструкторе. Приведенный пример очень прост, поэтому, скорее всего, в нем такое решение безопасно, но вы должны знать, что запуск потоков в конструкторе может создать изрядные проблемы — до завершения конструктора может быть запущена на выполнение другая задача, которая обратится к объекту в нестабильном состоянии. Это еще одна причина, по которой использование Executor предпочтительнее явного создания объектов Thread.
Иногда бывает разумно спрятать потоковый код внутри класса с помощью внутреннего класса, как показано здесь:
// concurrency/ThreadVariations java
// Создание потоков с использованием внутренних классов.
import java.util.concurrent.*,
import static net mindview.util.Print.*;
// Используем именованный внутренний класс, class InnerThreadl {
private int countDown = 5; private Inner inner, private class Inner extends Thread { Inner(String name) { super(name); startO,
}
public void run() { try {
while(true) {
print(this);
if(--countDown == 0) return; sleep(lO);
}
} catchdnterruptedException e) { »print("interrupted"):
}
}
public String toStringO {
return getNameO + ": " + countDown;
>
}
public InnerThreadKString name) { inner = new Inner(name);
Thread-0(4), Thread-КЗ), Thread-2(2), Thread-3(1),
Thread-1(5). Thread-2(4). Thread-3(3). Thread-4(2).
Thread-0(3), Thread-1(2), Thread-2(1), Thread-4(5),
Thread-0(2). Thread-Id). Thread-3(5). Thread-4(4),
Thread-Od). Thread-2(5). Thread-3(4), Thread-4(3).
// Используем безымянный внутренний класс: class InnerThread2 {
private int countDown = 5; private Thread t;
public InnerThread2(String name) { t = new Thread(name) {
public void run() { try {
while(true) {
print(this).
if(--countDown == 0) return, sleep(lO).
}
} catch(InterruptedException e) {
printCsleepO interrupted");
}
}
public String toStringO {
return getNameO + ". " + countDown;
}
}:
t startO;
}
}
// Используем именованную реализацию Runnable. class InnerRunnablel {
private int countDown = 5; private Inner inner,
private class Inner implements Runnable { Thread t;
Inner(String name) {
t = new Thread(this. name); t.startO;
}
public void runO { try {
while(true) {
print(this);
if(--countDown == 0) return; Ti mellnit .MILLISECONDS. si eep( 10);
}
} catch(InterruptedException e) {
printCsleepO interrupted");
}
}
public String toStringO {
return t.getNameO + ". " + countDown;
}
}
public InnerRunnableKString name) { inner = new Inner(name),