Эккель Брюс
Шрифт:
Впрочем, как только вы действительно проникаетесь идеей шаблона декоратора и начинаете использовать библиотеку в ситуациях, где требуется вся ее гибкость, вы извлекаете из библиотеки все большую выгоду, и даже требующийся для надстроек дополнительный код не мешает этому.
Параллельное выполнение
До настоящего момента мы имели дело исключительно с последовательным программированием. Все действия, выполняемые программой, выполнялись друг за другом, то есть последовательно.
Последовательное программирование способно решить многие задачи. Однако для некоторых зайач бывает удобно (и даже необходимо) организовать параллельное выполнение нескольких частей программы, чтобы создать у пользователя впечатление одновременного выполнения этих частей, или — если на компьютере установлено несколько процессоров — чтобы они действительно выполнялись одновременно.
Каждая из этих самостоятельных подзадач называется потоком (thread)1. Программа пишется так, словно каждый поток запускается сам по себе и использует процессор в монопольном режиме. На самом деле существует некоторый системный механизм, который обеспечивает совместное использованием процессора, но в основном думать об этом вам не придется.
Модель потоков (и ее поддержка в языке 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
Традиционный способ преобразования объекта Runnable в задачу заключается в передаче его конструктору Thread. Следующий пример показывает, как организовать выполнение LiftOff с использованием 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");
}