Основное понятие современных операционных систем — процесс (process). Как и все общие понятия, процесс трудно определить, да это и не входит в задачу книги. Можно понимать под процессом выполняющуюся (runnable) программу, но надо помнить о том, что у процесса есть несколько состояний. Процесс может в любой момент перейти к выполнению машинного кода другой программы, а также "заснуть" (sleep) на некоторое время, приостановив выполнение программы. Он может быть выгружен на диск. Количество состояний процесса и их особенности зависят от операционной системы.

Все современные операционные системы — многозадачные (multitasking), они запускают и выполняют сразу несколько процессов. Одновременно может работать, например, браузер, текстовый редактор, музыкальный проигрыватель. На экране дисплея открываются несколько окон, каждое из которых связано со своим работающим процессом.

Если на компьютере только один процессор, то он переключается с одного процесса на другой, создавая видимость одновременной работы. Переключение происходит по истечении одного или нескольких "тиков" (ticks). Размер тика зависит от тактовой частоты процессора и обычно имеет порядок 0,01 секунды. Процессам назначаются разные приоритеты (priority). Процессы с низким приоритетом не могут прервать выполнение процесса с более высоким приоритетом, они меньше занимают процессор и поэтому выполняются медленно, как говорят, "на фоне". Самый высокий приоритет у системных процессов, например у диспетчера (scheduler), который как раз и занимается переключением процессора с процесса на процесс. Такие процессы нельзя прерывать, пока они не закончат работу, иначе компьютер быстро придет в хаотическое состояние.

Каждому процессу выделяется определенная область оперативной памяти для размещения кода программы и ее данных — его адресное пространство. В эту же область записывается часть сведений о процессе, составляющая его контекст (context). Очень важно разделить адресные пространства разных процессов, чтобы они не могли изменить код и данные друг друга.

Операционные системы по-разному относятся к обеспечению защиты адресных пространств процессов. Системы MS Windows тщательно разделяют адресные пространства, тратя на это много ресурсов и времени. Это повышает надежность выполнения программы, но затрудняет создание процесса. Такие операционные системы плохо справляются с управлением большим числом процессов.

Операционные системы семейства UNIX меньше заботятся о защите памяти, но легче создают процессы и способны управлять сотней одновременно работающих процессов.

Кроме управления работой процессов операционная система должна обеспечить средства их взаимодействия: обмен сигналами и сообщениями, создание разделяемых несколькими процессами областей памяти и разделяемого исполнимого кода программы. Эти средства тоже требуют ресурсов и замедляют работу компьютера.

Работу многозадачной системы можно упростить и ускорить, если разрешить взаимодействующим процессам работать в одном адресном пространстве. Такие процессы называются threads. В русской литературе предлагаются различные переводы этого слова. Буквальный перевод — "нить", но мы не занимаемся прядильным производством. Часто переводят thread как "поток", но в этой книге мы говорим о потоке вво-да/вывода. Иногда просто говорят "тред", но в русском языке уже есть "тред-юнион". Встречается перевод "легковесный процесс", но в некоторых операционных системах, например Solaris, есть и thread и lightweight process. Остановимся на слове "подпроцесс".

Подпроцессы создают новые трудности для операционной системы — надо очень внимательно следить за тем, чтобы они не мешали друг другу при записи в общие участки памяти, — но зато облегчают взаимодействие подпроцессов.

Создание подпроцессов и управление ими — это дело операционной системы, но в язык Java введены некоторые средства для выполнения этих действий. Поскольку программы, написанные на Java, должны работать во всех операционных системах, эти средства позволяют выполнять только самые общие действия.

Когда операционная система запускает виртуальную машину Java для выполнения приложения, она создает один процесс с несколькими подпроцессами. Главный (main) подпроцесс выполняет байт-коды программы, а именно он сразу же обращается к методу main() приложения. Этот подпроцесс может породить новые подпроцессы, которые, в свою очередь, способны породить подпроцессы и т. д. Главным подпроцессом апплета является один из подпроцессов браузера, в котором апплет выполняется. Главный подпроцесс не играет никакой особой роли, просто он создается первым.

Подпроцесс может находиться в одном из шести состояний, помеченных следующими константами класса-перечисления (enum) Thread.state, статически вложенного в класс

Thread:

□ new — подпроцесс создан, но еще не запущен;

□ runnable — подпроцесс выполняется;

□ blocked — подпроцесс блокирован;

□ waiting — подпроцесс ждет окончания работы другого подпроцесса;

□ timed_waiting — подпроцесс ждет некоторое время окончания другого подпроцесса;

□ terminated — подпроцесс окончен.

Эти константы-объекты класса Thread. State.

Подпроцесс в Java создается и управляется методами класса Thread. После создания объекта этого класса одним из его конструкторов новый подпроцесс запускается методом start ( ).

Получить ссылку на текущий подпроцесс можно статическим методом

Thread.currentThread();

Состояние подпроцесса можно определить методом getState(), возвращающим одну из констант класса Thread. State.

Класс Thread реализует интерфейс Runnable, который описывает только один метод run ( ). Новый подпроцесс будет выполнять то, что записано в этом методе. Впрочем, класс Thread содержит только пустую реализацию метода run ( ), поэтому класс Thread не используется сам по себе, он всегда расширяется. При его расширении метод run () переопределяется.

Метод run () не содержит параметров, так как некому передавать аргументы в метод. Он не возвращает значения, его некуда передавать. Метод run () — обычный метод, к нему можно обратиться из программы, но в таком случае он будет выполняться в том же подпроцессе. Выполнение метода в новом подпроцессе осуществляет исполняющая система Java при запуске нового подпроцесса методом start ().

С массовым распространением многопроцессорных машин и многоядерных процессоров возникла потребность в более развитых средствах создания подпроцессов и управления ими. В состав JDK включили пакеты java.util.concurrent,

java.util.concurrent.atomic, java.util.concurrent.locks, содержащие интерфейсы и классы, облегчающие работу с подпроцессами.

Основу этих средств составляет интерфейс Callable, описывающий один метод call(). В отличие от метода run () метод call () возвращает результат — произвольный объект — и выбрасывает исключение класса Exception. Получение результата метода call () описано интерфейсом Future, методы get () которого дожидаются окончания работы метода call () и возвращают результат. Окончание работы метода отмечается логическим методом isDone(). Выполнение метода call() можно отменить методом cancel(), а проверить результат отмены — методом isCancelled ().

Интерфейс Future реализован классом ForkJoinTask, Это абстрактный класс, но в нем есть статические методы adapt (), возвращающие экземпляр этого класса.

Для того чтобы можно было таким же образом работать с объектами типа Runnable, интерфейсы Runnable и Future расширены интерфейсом RunnableFuture.

Интерфейс RunnableFuture реализован классом FutureTask, который обычно и используется для работы с объектами типа Runnable и Callable.

Для того чтобы дать разработчикам больше возможностей для создания подпроцессов, работающих с объектами типа Runnable, введен интерфейс Executor. Он описывает только один метод execute (Runnable), в реализации которого можно создать новый подпроцесс, хотя метод execute (Runnable) можно выполнить и в том же подпроцессе. Это дает возможность использовать освободившийся подпроцесс заново или создать пул подпроцессов и выбирать свободные подпроцессы из пула.

Более развитые средства содержит расширение интерфейса Executor — интерфейс ExecutorService. Его методы submit ( ) выполняют работу объектов типа Runnable или Callable и возвращают объект типа Future, что позволяет следить за выполнением подпроцесса и управлять им.

Интерфейс ExecutorService описывает более десятка методов. Чтобы облегчить его реализацию создан абстрактный класс AbstractExecutorService, для использования которого достаточно переопределить методы newTaskFor(), используемые затем методами

submit( ) .

Класс AbstractExecutorService расширен классом ThreadPoolExecutor, создающим и использующим пул подпроцессов, и классом ForkJoinPool, способным выполнять работу объектов типа ForkJoinTask, Runnable или Callable методами execute ( ), invoke () и submit (). Как видно из названия класса, он сразу создает пул подпроцессов, а затем выполняет задачи, используя свободные подпроцессы из пула.

Пример использования интерфейса Executor приведен в листинге 26.8.

Основной метод использования многопроцессорности заключается в том, что большая задача рекурсивно разбивается на более мелкие независимые подзадачи, пока число подзадач не сравняется с числом процессоров или размер подзадачи не станет приемлемым. Затем задачи решаются параллельно несколькими процессорами, а результаты решения подзадач обратными шагами рекурсии собираются в решение всей задачи. Так, например, выполняется алгоритм быстрой сортировки. Для выполнения этих действий в пакете java.util.concurrent имеются абстрактные классы RecursiveTask и RecursiveAction. В них оставлен абстрактным метод compute (), переопределяя который можно задать рекурсивное разбиение задачи на подзадачи.

Пример использования классов ForkJoinPool, ForkJoinTask и RecursiveAction приведен в стандартной поставке Java SE, в каталоге $JAVA_HOME/sample/forkjoin/mergesort.

Кроме того, в пакете java.util.concurrent есть класс Executors, содержащий статические методы, создающие объекты типа ExecutorService для работы в новых подпроцессах.

Итак, задать действия создаваемого подпроцесса можно множеством способов: расширить класс Thread, реализовать интерфейс Runnable, создать объекты типа Executor или его подтипов. Первый способ позволяет использовать методы класса Thread для управления подпроцессом. Второй способ применяется в тех случаях, когда надо только реализовать метод run () или класс, создающий подпроцесс, уже расширяет какой-то другой класс. Третий способ удобен для создания пула подпроцессов.

Посмотрим, какие конструкторы и методы содержит класс Thread.

Класс Thread

В классе Thread восемь конструкторов. Основной из них,

Thread(ThreadGroup group, Runnable target, String name, long stackSize);

создает подпроцесс с именем name, принадлежащий группе group и выполняющий метод run() объекта target. Последний параметр, stackSize, задает размер стека и сильно зависит от операционной системы.

Все остальные конструкторы обращаются к нему с тем или иным параметром, равным

null:

□ Thread () — создаваемый подпроцесс будет выполнять свой метод run ();

в–Ў Thread(Runnable target);

в–Ў Thread(Runnable target, String name);

в–Ў Thread(String name);

в–Ў Thread(ThreadGroup group, Runnable target, String name);

в–Ў Thread(ThreadGroup group, Runnable target);

в–Ў Thread(ThreadGroup group, String name).

Имя подпроцесса name не имеет никакого значения, оно не используется виртуальной машиной Java и применяется только для различения подпроцессов в программе.

После создания подпроцесса его надо запустить методом start (). Виртуальная машина Java начнет выполнять метод run () этого объекта-подпроцесса.

Подпроцесс завершит работу после выполнения метода run (). Для уничтожения объекта-подпроцесса вслед за этим он должен присвоить значение null.

Выполняющийся подпроцесс можно приостановить статическим методом

sleep(long ms);

на ms миллисекунд. Этот метод мы уже использовали в предыдущих главах. Если вычислительная система способна отсчитывать наносекунды, то можно приостановить подпроцесс с точностью до наносекунд методом

sleep(long ms, int nanosec);

В листинге 22.1 приведен простейший пример. Главный подпроцесс создает два подпроцесса с именами Thread 1 и Thread 2, выполняющих один и тот же метод run(). Этот метод просто выводит 20 раз текст на экран, а затем сообщает о своем завершении.

Листинг 22.1. Два подпроцесса, запущенные из главного подпроцесса

class OutThread extends Thread{

private String msg;

OutThread(String s, String name){ super(name); msg = s;

}

public void run(){

for(int i = 0; i < 20; i++){

// try{

// Thread.sleep(100);

// }catch(InterruptedException ie){}

System.out.print(msg + " ");

}

System.out.println("End of " + getName());

}

}

class TwoThreads{

public static void main(String[] args){

new OutThread("HIP", "Thread 1").start();

new OutThread(Mhopn, "Thread 2").start();

System.out.println();

}

}

На рис. 22.1 показан результат двух запусков программы листинга 22.1. Как видите, в первом случае подпроцесс Thread 1 успел отработать полностью до переключения процессора на выполнение второго подпроцесса. Во втором случае работа подпроцесса Thread 1 была прервана, процессор переключился на выполнение подпроцесса Thread 2, успел выполнить его полностью, а затем переключился обратно на выполнение подпроцесса Thread 1 и завершил его.

Уберем в листинге 22.1 комментарии, задержав тем самым выполнение каждой итерации цикла на 0,1 секунды. Пустая обработка исключения InterruptedException означает, что мы игнорируем попытку прерывания работы подпроцесса. На рис. 22.2 показан результат двух запусков программы. Как видите, процессор переключается с одного подпроцесса на другой, но в одном месте регулярность переключения нарушается и ранее запущенный подпроцесс завершается позднее.

Рис. 22.1. Два подпроцесса работают без задержки
Рис. 22.2. Подпроцессы работают с задержкой

Как же добиться согласованности, как говорят, синхронизации (synchronization) подпроцессов? Обсудим это позже, а пока покажем еще два варианта создания той же самой программы.

В листинге 22.2 приведен второй вариант программы: сам класс TwoThreads2 является расширением класса Thread, а метод run () реализуется прямо в нем.

Листинг 22.2. Класс расширяет Thread !

class TwoThreads2 extends Thread{

private String msg;

TwoThreads2(String s, String name){ super(name); msg = s;

}

public void run(){

for(int i = 0; i < 20; i++){ try{

Thread.sleep(100);

}catch(InterruptedException ie){}

System.out.print(msg + " ");

}

System.out.println("End of " + getName());

}

public static void main(String[] args){

new TwoThreads2("HIP", "Thread 1").start(); new TwoThreads2("hop", "Thread 2").start();

System.out.println();

}

}

Третий вариант: класс TwoThreads3 реализует интерфейс Runnable. Этот вариант записан в листинге 22.3. Здесь нельзя использовать методы класса Thread, но зато класс TwoThreads3 может быть расширением другого класса. Например, можно сделать его апплетом, расширив класс Applet или JApplet.

Листинг 22.3. Реализация интерфейса Runnable

class TwoThreads3 implements Runnable{

private String msg;

TwoThreads3(String s){ msg = s; } public void run(){

for(int i = 0; i < 20; i++){ try{

Thread.sleep(100); }catch(InterruptedException ie){} System.out.print(msg + " ");

}

System.out.println("End of thread.");

} public static void main(String[] args){

new Thread(new TwoThreads3("HIP"), "Thread 1").start(); new Thread(new TwoThreads3("hop"), "Thread 2").start();

System.out.println();

}

}

Чаще всего в новом подпроцессе задаются бесконечные действия, выполняющиеся на фоне основных: проигрывается музыка, на экране вращается анимированный логотип фирмы, бежит рекламная строка. Для реализации такого подпроцесса в методе run () задается бесконечный цикл, останавливаемый после того, как объект-подпроцесс получит значение null.

В листинге 22.4 показан четвертый вариант той же самой программы, в которой метод run () выполняется до тех пор, пока текущий объект-подпроцесс th совпадает с объектом go, запустившим текущий подпроцесс. Для прекращения его выполнения предусмотрен метод stop(), к которому обращается главный подпроцесс. Это стандартная конструкция, рекомендуемая документацией Java SE JDK. Главный подпроцесс в данном примере только создает объекты-подпроцессы, ждет 1 секунду и останавливает их.

Листинг 22.4. Прекращение работы подпроцессов

class TwoThreads5 implements Runnable{

private String msg; private Thread go;

TwoThreads5(String s){ msg = s;

go = new Thread(this); go.start();

}

public void run(){

Thread th = Thread.currentThread(); while(go == th){ try{

Thread.sleep(100); }catch(InterruptedException ie){} System.out.print(msg + " ");

}

System.out.println("End of thread.");

}

public void stop(){ go = null; }

public static void main(String[] args){ TwoThreads5 th1 = new TwoThreads5("HIP"); TwoThreads5 th2 = new TwoThreads5("hop"); try{

Thread.sleep(1000); }catch(InterruptedException ie){}

th1.stop(); th2.stop();

System.out.println();

}

}

Синхронизация подпроцессов

Основная сложность при написании программ, в которых работают несколько подпроцессов, — это согласовать совместную работу подпроцессов с общими ячейками памяти.

Классический пример — банковская транзакция, в которой изменяется остаток на счету клиента с номером numDep. Предположим, что для ее выполнения запрограммированы такие действия:

Deposit myDep = getDeposit(numDep); // Получаем счет с номером numDep int rest = myDep.getRest(); // Получаем остаток на счету myDep

rest += sum; // Изменяем остаток на величину sum

myDep.setRest(rest); // Заносим новый остаток на счет myDep

Пусть на счету лежит 1000 рублей. Мы решили снять со счета 500 рублей, а в это же время поступил почтовый перевод на 1500 рублей. Эти действия выполняют разные подпроцессы, но изменяют они один и тот же счет myDep с номером numDep. Посмотрев еще раз на рис. 22.1 и 22.2, вы поверите, что последовательность действий может сложиться так. Первый подпроцесс проделает вычитание 1000 - 500, в это время второй подпроцесс выполнит все три действия и запишет на счет 1000 + 1500 = 2500 рублей, после чего первый подпроцесс выполнит свое последнее действие setRest () и у нас на счету окажется 500 рублей. Вряд ли вам понравится такое выполнение двух транзакций.

В языке Java принят выход из этого положения, называемый в теории операционных систем монитором (monitor). Он заключается в том, что подпроцесс блокирует объект, с которым работает, чтобы другие подпроцессы не могли обратиться к данному объекту, пока блокировка не будет снята. В нашем примере первый подпроцесс должен вначале заблокировать счет myDep, затем полностью выполнить всю транзакцию и снять блокировку. Второй подпроцесс приостановится и станет ждать, пока блокировка не будет снята, после чего начнет работать с объектом myDep.

Все это делается одним оператором synchronized (){}, как показано ниже:

Deposit myDep = getDeposit(numDep); synchronized(myDep){

int rest = myDep.getRest();

rest += sum;

myDep.setRest(rest);

}

В заголовке оператора synchronized в скобках указывается ссылка на объект, который будет заблокирован перед выполнением блока. Объект будет недоступен для других подпроцессов, пока выполняется блок. После выполнения блока блокировка снимается.

Если при написании какого-нибудь метода оказалось, что в блок synchronized входят все операторы этого метода, то можно просто пометить метод словом synchronized, сделав его синхронизированным (synchronized):

synchronized int getRest(){

// Тело метода

}

synchronized void setRest(int rest){

// Тело метода

}

В этом случае блокируется объект, выполняющий метод, т. е. объект this. Если все методы, к которым не должны одновременно обращаться несколько подпроцессов, помечены synchronized, то оператор synchronized(){} уже не нужен. Теперь если один подпроцесс выполняет синхронизированный метод объекта, то другие подпроцессы уже не могут обратиться ни к одному синхронизированному методу того же самого объекта.

Приведем простейший пример. Метод run() в листинге 22.5 выводит строку "Hello, World!" с задержкой в 1 секунду между словами. Этот метод выполняется двумя подпроцессами, работающими с одним объектом th. Программа выполняется два раза. Первый раз метод run () не синхронизирован, второй раз синхронизирован, его заголовок показан в листинге 22.5 как комментарий. Результат выполнения программы представлен на рис. 22.3.

Рис. 22.3. Синхронизация метода

Листинг 22.5. Синхронизация метода

class TwoThreads4 implements Runnable{ public void run(){

// synchronized public void run(){

System.out.print("Hello, "); try{

Thread.sleep(1000); }catch(InterruptedException ie){}

System.out.println("World!");

}

public static void main(String[] args){

TwoThreads4 th = new TwoThreads4(); new Thread(th).start(); new Thread(th).start();

}

}

Действия, входящие в синхронизированный блок или метод, образуют критический участок (critical section) программы. Несколько подпроцессов, собирающихся выполнять критический участок, встают в очередь. Это замедляет работу программы, поэтому для быстроты ее выполнения критических участков должно быть как можно меньше, и они должны быть как можно короче.

Многие методы классов Java 2 JDK синхронизированы. Обратите внимание, что на рис. 22.1 слова выводятся вперемешку, но каждое слово выводится полностью. Это происходит потому, что метод print () класса PrintStream синхронизирован, при его выполнении выходной поток System.out блокируется до тех пор, пока метод print ( ) не закончит свою работу.

Итак, мы можем легко организовать последовательный доступ нескольких подпроцессов к полям одного объекта с помощью оператора synchronized(){}. Синхронизация обеспечивает взаимно исключающее (mutually exclusive) выполнение подпроцессов. Но что делать, если нужен совместный доступ нескольких подпроцессов к общим объектам? Для этого в Java существует механизм ожидания и уведомления (wait-notify).

Согласование работы нескольких подпроцессов

Возможность создания многопоточных программ заложена в язык Java с самого его создания. В каждом объекте есть три метода wait() и один метод notify(), которые позволяют приостановить работу подпроцесса с этим объектом, разрешают другому подпроцессу поработать с объектом, а затем уведомляют (notify) первый подпроцесс о возможности продолжения работы. Эти методы определены прямо в классе Object и наследуются всеми классами.

С каждым объектом связано множество подпроцессов, ожидающих доступа к объекту (wait set). Вначале этот "зал ожидания" пуст.

Основной метод wait(long millisec) приостанавливает текущий подпроцесс this, работающий с объектом, на millisec миллисекунд и переводит его в "зал ожидания", в множество ожидающих подпроцессов. Обращение к этому методу допускается только в синхронизированном блоке или методе, чтобы быть уверенными в том, что с объектом работает только один подпроцесс. По истечении millisec или после того, как объект пошлет уведомление методом notify(), подпроцесс готов возобновить работу. Если аргумент millisec равен 0, то время ожидания не определено и возобновление работы подпроцесса возможно только после того, как объект пошлет уведомление методом

notify().

Отличие данного метода от метода sleep () в том, что метод wait () снимает блокировку с объекта. С объектом может работать один из подпроцессов из "зала ожидания", обычно тот, который ждал дольше всех, хотя это не гарантируется спецификацией Java Language Specification.

Второй метод, wait ( ), эквивалентен wait(0). Третий метод, wait(long millisec, int nanosec) , уточняет задержку на nanosec наносекунд, если их сумеет отсчитать операционная система.

Метод notify() выводит из "зала ожидания" только один, произвольно выбранный подпроцесс. Метод notifyAll () выводит из состояния ожидания все подпроцессы. Эти методы тоже должны выполняться в синхронизированном блоке или методе.

Как же применить все это для согласованного доступа к объекту? Как всегда, лучше всего объяснить это на примере.

Обратимся снова к схеме "поставщик-потребитель", уже использованной в главе 20. Один подпроцесс, поставщик, производит вычисления, другой, потребитель, ожидает результаты этих вычислений и использует их по мере поступления. Подпроцессы передают информацию через общий экземпляр st класса Store.

Работа этих подпроцессов должна быть согласована. Потребитель обязан ждать, пока поставщик не занесет результат вычислений в объект st, а поставщик должен ждать, пока потребитель не возьмет этот результат.

Для простоты поставщик просто заносит в общий объект класса Store целые числа, а потребитель лишь забирает их.

В листинге 22.6 класс Store не обеспечивает согласования получения и выдачи информации. Результат работы показан на рис. 22.4.

Листинг 22.6. Несогласованные подпроцессы

class Store{

private int inform;

synchronized public int getInform(){ return inform; } synchronized public void setInform(int n){ inform = n; }

}

class Producer implements Runnable{

private Store st; private Thread go;

Producer(Store st){ this.st = st; go = new Thread(this); go.start();

}

public void run(){ int n = 0;

Thread th = Thread.currentThread();

Wwile (go == th){ st.setInform(n);

System.out.print("Put: " + n + " ");

n++;

}

}

public void stop(){ go = null; }

}

class Consumer implements Runnable{

private Store st; private Thread go;

Consumer(Store st){ this.st = st; go = new Thread(this); go.start();

}

public void run(){

Thread th = Thread.currentThread();

while (go == th) System.out.println("Got: " + st.getInform());

}

public void stop(){ go = null; }

}

class ProdCons{

public static void main(String[] args){

Store st = new Store();

Producer p = new Producer(st);

Consumer c = new Consumer(st); try{

Thread.sleep(30);

}catch(InterruptedException ie){} p.stop(); c.stop();

}

}

Рис. 22.4. Несогласованная работа двух подпроцессов

В листинге 22.7 в класс Store внесено логическое поле ready, отмечающее процесс получения и выдачи информации. Когда новая порция информации получена от поставщика Producer, в поле ready заносится значение true, получатель Consumer может забирать эту порцию информации. После выдачи информации переменная ready становится равной

false.

Но этого мало. То, что получатель может забрать продукт, не означает, что он действительно заберет его. Поэтому в конце метода setInform() получатель уведомляется о поступлении продукта методом notify(). Пока поле ready не примет нужное значение, подпроцесс переводится в "зал ожидания" методом wait (). Результат работы программы с обновленным классом Store показан на рис. 22.5.

Листинг 22.7. Согласование получения и выдачи информации

class Store{

private int inform = -1; private boolean ready; synchronized public int getInform(){ try{

if (!ready) wait(); ready = false; return inform;

}catch(InterruptedException ie){

}finally{ notify();

}

return -1;

}

synchronized public void setInform(int n){ if (ready) try{

wait ();

}catch(InterruptedException ie){} inform = n; ready = true; notify();

}

}

Рис. 22.5. Согласованная работа подпроцессов

Поскольку уведомление поставщика в методе getInform( ) должно происходить уже после отправки информации оператором return inform, оно включено в блок finally{}.

Обратите внимание: сообщение "Got: 0" отстает на один шаг от действительного получения информации.

Поскольку схема "поставщик-потребитель" часто используется в программировании, в пакете java.util.concurrent определено несколько классов-коллекций, обеспечивающих взаимодействие подпроцессов, работающих с ними. Они реализуют интерфейсы

BlockingQueue или BlockingDeque, изложенные в главе 6, или с помощью массива- так

делают классы ArrayBlockingQueue и ArrayBlockingDeque; или с помощью линейного списка — классы ConcurrentLinkedQueue и ConcurrentLinkedDeque; или вообще без коллекции, а только с одним элементом - класс SynchronousQueue.

Для нашей реализации схемы "поставщик-потребитель" хорошо подходит класс SynchronousQueue. Вот как можно использовать его в классе Store:

class Store{

private SynchronousQueue st = new SynchronousQueue<>(); public T getInform(){ try{

return st.take();

}catch(InterruptedException e){} return null;

}

public void setInform(T t){ try{

st.put(t);

}catch(InterruptedException e){}

}

}

Однако класс SynchronousQueue может взять на себя всю работу, которую у нас выполняет класс Store, и мы можем совсем обойтись без нашего собственного класса Store. В листинге 22.8 записана программа, реализующая схему "поставщик-потребитель" только классом SynchronousQueue.

Листинг 22.8. Согласование стандартным классом SynchronousQueue

import j ava.util.concurrent.SynchronousQueue; public class ProdCons{

public static void main(String[] args){ SynchronousQueue st = new SynchronousQueue<>(); Consumer c = new Consumer(st);

Producer p = new Producer(st); try{

Thread.sleep(30);

}catch(InterruptedException ie){} p.stop(); c.stop();

}

}

class Producer implements Runnable{ private SynchronousQueue st; private Thread go;

Producer(SynchronousQueue st){ this.st = st; go = new Thread(this); go.start();

}

public void run(){ int t = 0;

Thread th = Thread.currentThread(); while (go == th){ try{

st.put(t);

}catch(InterruptedException ie){}

System.out.print("Put: " + t + " ");

t++;

}

}

public void stop(){ go = null; }

}

class Consumer implements Runnable{ private SynchronousQueue st; private Thread go;

Consumer(SynchronousQueue st){ this.st = st; go = new Thread(this); go.start();

}

public void run(){

Thread th = Thread.currentThread();

try{

while (go == th) System.out.println("Got: " + st.take());

}catch(InterruptedException ie){}

}

public void stop(){ go = null; }

}

Приоритеты подпроцессов

Планировщик подпроцессов виртуальной машины Java назначает каждому подпроцессу одинаковое время выполнения процессором, переключаясь с подпроцесса на подпроцесс по истечении этого времени. Иногда необходимо выделить какому-то подпроцессу больше или меньше времени по сравнению с другим подпроцессом. В таком случае можно задать подпроцессу больший или меньший приоритет.

В классе Thread есть три целые статические константы, задающие приоритеты:

□ norm_priority — обычный приоритет, который получает каждый подпроцесс при запуске, его числовое значение 5;

□ min_priority — наименьший приоритет, его значение 1;

□ max_priority — наивысший приоритет, его значение 10.

Кроме этих значений можно задать любое промежуточное значение от 1 до 10, но надо помнить о том, что процессор будет переключаться между подпроцессами с одинаковым высшим приоритетом, а подпроцессы с меньшим приоритетом не станут выполняться, если только не приостановлены все подпроцессы с высшим приоритетом. Поэтому для повышения общей производительности следует приостанавливать время от времени методом sleep () подпроцессы с высоким приоритетом.

Установить тот или иной приоритет можно в любое время методом setPriority(int newPriority), если подпроцесс имеет право изменить свой приоритет. Проверить наличие такого права можно методом checkAccess ( ), который выбрасывает исключение класса SecurityException, если подпроцесс не может изменить свой приоритет.

Порожденные подпроцессы будут иметь тот же приоритет, что и подпроцесс-родитель.

Итак, подпроцессы, как правило, должны работать с приоритетом norm_priority. Подпроцессы, большую часть времени ожидающие наступления какого-нибудь события, например нажатия пользователем кнопки Выход, могут получить более высокий приоритет max_priority. Подпроцессы, выполняющие длительную работу, например установку сетевого соединения или отрисовку изображения в памяти при двойной буферизации, могут работать с низшим приоритетом min_priority.

Подпроцессы-демоны

Работа программы начинается с выполнения метода main() главным подпроцессом. Этот подпроцесс может породить другие подпроцессы, они в свою очередь способны породить собственные подпроцессы. После этого главный подпроцесс ничем не будет отличаться от остальных подпроцессов. Он не следит за порожденными им подпроцессами, не ждет от них никаких сигналов. Главный подпроцесс может завершиться, а программа будет продолжать работу, пока не закончит работу последний подпроцесс.

Это правило не всегда удобно. Например, какой-то из подпроцессов может приостановиться, ожидая сетевого соединения, которое никак не может наступить. Пользователь, не дождавшись соединения, прекращает работу главного подпроцесса, но программа продолжает работать.

Такие случаи можно учесть, объявив некоторые подпроцессы демонами (daemons). Это понятие не совпадает с понятием демона в UNIX. Просто программа завершается по окончании работы последнего пользовательского (user) подпроцесса, не дожидаясь окончания работы демонов. Демоны будут принудительно завершены исполняющей системой Java.

Объявить подпроцесс демоном можно сразу после его создания, перед запуском. Это делается методом setDaemon(true). Данный метод обращается к методу checkAccess() и может выбросить SecurityException. Изменить статус демона после запуска подпроцесса уже нельзя.

Все подпроцессы, порожденные демоном, тоже будут демонами. Для изменения их статуса необходимо обратиться к методу setDaemon ( false).

Группы подпроцессов

Подпроцессы объединяются в группы. В начале работы программы исполняющая система Java создает группу подпроцессов с именем main. Все подпроцессы по умолчанию попадают в эту группу.

В любое время программа может создать новые группы подпроцессов и подпроцессы, входящие в эти группы. Вначале создается группа — экземпляр класса ThreadGroup — конструктором

ThreadGroup(String name);

При этом группа получает имя, заданное аргументом name. Затем этот экземпляр указывается при создании подпроцессов в конструкторах класса Thread. Все подпроцессы попадут в группу с именем, заданным при создании группы.

Группы подпроцессов могут образовать иерархию. Одна группа порождается от другой конструктором

ThreadGroup(ThreadGroup parent, String name);

Группы подпроцессов используются главным образом для задания приоритетов подпроцессам внутри группы. Изменение приоритетов внутри группы не будет влиять на приоритеты подпроцессов вне иерархии этой группы. Каждая группа имеет максимальный приоритет, устанавливаемый методом setMaxPriority(int maxPri) класса ThreadGroup. Ни один подпроцесс из этой группы не может превысить значения maxPri, но приоритеты подпроцессов, заданные до установки maxPri, не меняются.

Заключение

Технология Java по своей сути — многозадачная технология, основанная на threads. Поэтому, конструируя программу для Java, следует все время помнить, что она будет выполняться в многозадачной среде. Надо ясно представлять себе, что будет, если программа начнет выполняться одновременно несколькими подпроцессами, выделять критические участки и синхронизировать их.

С другой стороны, если программа осуществляет несколько действий, следует подумать, не сделать ли их выполнение одновременным, создав дополнительные подпроцессы и распределив их приоритеты.

Вопросы для самопроверки

1. Что такое процесс и подпроцесс в современных операционных системах?

2. Почему языки программирования, как правило, не содержат средств управления подпроцессами?

3. Зачем в язык Java введены средства создания и управления подпроцессами?

4. Какими способами можно создать и запустить подпроцесс?

5. Когда подпроцесс заканчивает свою работу?

6. Как можно остановить подпроцесс?

7. Что такое "монитор" в теории операционных систем?

8. Каким образом монитор реализуется в Java?

ГЛАВА 23