Основное понятие современных операционных систем — процесс (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
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
Producer p = new Producer(st); try{
Thread.sleep(30);
}catch(InterruptedException ie){} p.stop(); c.stop();
}
}
class Producer implements Runnable{ private SynchronousQueue
Producer(SynchronousQueue
}
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
Consumer(SynchronousQueue
}
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