Философия Java3
Шрифт:
Модель потоков (и ее поддержка в языке Java) является программным механизмом, упрощающим одновременное выполнение нескольких операций в одной и той же программе. Процессор периодически вмешивается в происходящие события, выделяя каждому потоку некоторой отрезок времени. Для каждого потока все выглядит так, словно процессор используется в монопольном режиме, но на самом деле время процессора разделяется между всеми существующими в программе потоками (исключение составляет ситуация, когда программа действительно выполняется на многопроцессорном компьютере). Однако при использовании потоков вам не нужно задумываться об этих тонкостях — код не зависит от того, на скольких процессорах
Задачи
Программный поток представляет некоторую задачу или операцию, поэтому нам понадобятся средства для описания этой задачи. Их предоставляет интерфейс Runnable. Чтобы определить задачу, реализуйте Runnable и напишите метод run, содержащий код выполнения нужных действий.
Например, задача LiftOff выводит обратный отсчет перед стартом:
//• concurrency/LiftOff java
// Реализация интерфейса Runnable
public class LiftOff implements Runnable {
protected int countDown =10; // Значение по умолчанию
private static int taskCount = 0,
private final int id = taskCount++;
public LiftOffО {}
public LiftOff(int countDown) {
this countDown = countDown;
}
public String status О {
return "#" + id + "(" +
(countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run {
while(countDown-- > 0) {
System.out.pri nt(status); Thread.yieldO;
}
}
} ///:-
По идентификатору id различаются экземпляры задачи. Поле объявлено с ключевым словом final, поскольку оно не будет изменяться после инициализации.
Метод run обычно содержит некоторый цикл, который продолжает выполняться до тех пор, пока не будет достигнуто некоторое завершающее условие. Следовательно, вы должны задать условие выхода из цикла (например, просто вернуть управление командой return). Часто run выполняется в виде бесконечного цикла, а это означает, что при отсутствии завершающего условия выполнение будет продолжаться бесконечно (позднее в этой главе вы узнаете, как организовать безопасное завершение задач).
Вызов статического метода Thread.yield в run обращен к планировщику потоков (часть потокового механизма Java, обеспечивающая переключение процессора между потоками). Фактически он означает, что очередная важная часть цикла была выполнена и теперь можно на время переключиться на другую задачу. Вызов yield не обязателен, но в данном примере он обеспечивает более интересные результаты: вы с большей вероятностью увидите, что программный поток прерывает и возобновляет свою работу.
В следующем примере метод run не выделяется в отдельный программный поток, а просто вызывается напрямую в main (впрочем, поток все же используется — тот, который всегда создается для main):
//: concurrency/MainThread.java
public class MainThread {
public static void main(String[] args) { Liftoff launch = new LiftOffO; launch.run;
}
} /* Output-
#0(9). #0(8). #0(7), #0(6). #0(5). #0(4). #0(3). #0(2). #0(1). #0(Liftoff!).
*///:-
Класс, реализующий Runnable, должен содержать метод run, но ничего особенного в этом методе нет — он не обладает никакими особыми потоковыми возможностями. Чтобы использовать многопоточное выполнение, необходимо явно связать задачу с потоком.
Класс Thread
Традиционный
//: concurrency/BasicThreads.java
// Простейший вариант использования класса Thread.
public class BasicThreads { public static void main(String[] args) { Thread t = new Thread (new LiftOffO); t.startO;
System.out.println("Waiting for LiftOff");
}
} /* Output: (90* match)
Waiting for LiftOff
#0(9). #0(8). #0(7). #0(6). #0(5). #0(4). #0(3). #0(2). #0(1). #0(Liftoff!).
*///:-
Конструктору Thread передается только объект Runnable. Метод start выполняет необходимую инициализацию потока, после чего вызов метода run интерфейса Runnable запускает задачу на выполнение в новом потоке.
Из выходных данных видно, что вызов start быстро возвращает управление (сообщение «Waiting for LiftOff» появляется до завершения отсчета). В сущности, мы вызываем LiftOff.runQ, а этот метод еще не завершил свое выполнение;
но, поскольку LiftOff.run выполняется в другом потоке, в потоке main в это время можно выполнять другие операции. (Данная возможность не ограничивается потоком main — любой поток может запустить другой поток.) Получается, что программа выполняет два метода сразу — main и LiftOff. run.
В программе можно легко породить дополнительные потоки для выполнения дополнительных задач:
// concurrency/MoreBasicThreads java
// Добавление новых потоков
public class MoreBasicThreads {
public static void main(String[] args) { for(int i = 0, l < 5, i++)
new Thread(new LiftOffO) startO. System.out println("Waiting for LiftOff"),
#2(7), #3(7) #4(5), #0(4) #1(2), #2(2) #l(Liftoff!) *///:-#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(Liftoff!),
} /* Output. (Пример) Waiting for LiftOff #0(9), #1(9), #2(9), #3(9)
Из выходных данных видно, что задачи выполняются одновременно друг с другом, с поочередной активизацией и выгрузкой потоков. Переключение осуществляется автоматически планировщиком потоков. Если на компьютере установлено несколько процессоров, планировщик потоков автоматически распределяет потоки между разными процессорами.
При разных запусках программы будут получены разные результаты, поскольку работа планировщика потоков недетерминирована. Более того, вы наверняка увидите значительные различия в результатах работы данной программы-примера, запуская ее на различных версиях пакета JDK. К примеру, предыдущие версии JVM не слишком часто выполняли квантование времени, соответственно, поток 1 мог первым закончить свой цикл, затем все свои итерации произвел бы поток 2, и т. д. Фактически то же самое получилось бы, если бы вызывалась процедура, выполняющая все циклы одновременно, за тем исключением, что запуск совокупности потоков требует больших издержек. Более поздние версии JDK обеспечивают более качественное квантование, и каждый поток регулярно получает свою долю внимания. Как правило, Sun не упоминает о подобных изменениях, так что рассчитывать на определенные «правила поведения» потоков не стоит. Лучше всего при написании кода с потоками занять максимально консервативную позицию.