Microsoft Visual J++. Создание приложений и аплетов на языке Java. Часть 2© Александр Фролов, Григорий ФроловТом 32, М.: Диалог-МИФИ, 1997, 288 стр. Синхронизация задачМультизадачный режим работы открывает новые возможности для программистов, однако за эти возможности приходится расплачиваться усложнением процесса проектирования приложения и отладки. Основная трудность, с которой сталкиваются программисты, никогда не создававшие ранее мультизадачные приложения, это синхронизация одновременно работающих задач. Для чего и когда она нужна? Однозадачная программа, такая, например, как программа MS-DOS, при запуске получает в монопольное распоряжение все ресурсы компьютера. Так как в однозадачной системе существует только один процесс, он использует эти ресурсы в той последовательности, которая соответствует логике работы программы. Процессы и задачи, работающие одновременно в мультизадачной системе, могут пытаться обращаться одновременно к одним и тем же ресурсам, что может привести к неправильной работе приложений. Поясним это на простом примере. Пусть мы создаем программу, выполняющую операции с банковским счетом. Операция снятия некоторой суммы денег со счета может происходить в следующей последовательности: · на первом шаге проверяется общая сумма денег, которая хранится на счете; · если общая сумма равна или превышает размер снимаемой суммы денег, общая сумма уменьшается на необходимую величину; · значение остатка записывается на текущий счет. Если операция уменьшения текущего счета выполняется в однозадачной системе, то никаких проблем не возникнет. Однако представим себе, что два процесса пытаются одновременно выполнить только что описанную операцию с одним и тем же счетом. Пусть при этом на счету находится 5 млн. долларов, а оба процесса пытаются снять с него по 3 млн. долларов. Допустим, события разворачиваются следующим образом: · первый процесс проверяет состояние текущего счета и убеждается, что на нем хранится 5 млн. долларов; · второй процесс проверяет состояние текущего счета и также убеждается, что на нем хранится 5 млн. долларов; · первый процесс уменьшает счет на 3 млн. долларов и записывает остаток (2 млн. долларов) на текущий счет; · второй процесс выполняет ту же самую операцию, так как после проверки считает, что на счету по-прежнему хранится 5 млн. долларов. В результате получилось, что со счета, на котором находилось 5 млн. долларов, было снято 6 млн. долларов, и при этом там осталось еще 2 млн. долларов! Итого - банку нанесен ущерб в 3 млн. долларов. Как же составить программу уменьшения счета, чтобы она не позволяла вытворять подобное? Очень просто - на время выполнения операций над счетом одним процессом необходимо запретить доступ к этому счету со стороны других процессов. В этом случае сценарий работы программы должен быть следующим: · процесс блокирует счет для выполнения операций другими процессами, получая его в монопольное владение; · процесс проводит процедуру уменьшения счета и записывает на текущий счет новое значение остатка; · процесс разблокирует счет, разрешая другим процессам выполнение операций. Когда первый процесс блокирует счет, он становится недоступен другим процессам. Если второй процесс также попытается заблокировать этот же счет, он будет переведен в состояние ожидания. Когда первый процесс уменьшит счет и на нем останется 2 млн. долларов, второй процесс будет разблокирован. Он проверит остаток, убедится, что сумма недостаточна и не будет проводить операцию. Таким образом, в мультизадачной среде необходима синхронизация задач при обращении к критическим ресурсам. Если над такими ресурсами будут выполняться операции в неправильной последовательности, это приведет к возникновению трудно обнаруживаемых ошибок. В языке программирования Java предусмотрено несколько средств для синхронизации задач, которые мы сейчас рассмотрим. Синхронизация методовВозможность синхронизации как бы встроена в каждый объект, создаваемый приложением Java. Для этого объекты снабжаются защелками, которые могут быть использованы для блокировки задач, обращающихся к этим объектам. Чтобы воспользоваться защелками, вы можете объявить соответствующий метод как synchronized, сделав его синхронизированным: public synchronized void decrement() { . . . } При вызове синхронизированного метода соответствующий ему объект (в котором он определен) блокируется для использования другими синхронизированными методами. В результате предотвращается одновременная запись двумя методами значений в область памяти, принадлежащую данному объекту. Использование синхронизированных методов - достаточно простой способ синхронизации задач, обращающихся к общим критическим ресурсам, наподобие описанного выше банковского счета. Заметим, что не обязательно синхронизовать весь метод - можно выполнить синхронизацию только критичного фрагмента кода. . . . synchronized(Account) { if(Account.check(3000000)) Account.decrement(3000000); } . . . Здесь синхронизация выполняется для объекта Account. Блокировка задачиСинхронизированная задача, определенная как метод типа synchronized, может переходить в заблокированное состояние автоматически при попытке обращения к ресурсу, занятому другим синхронизированным методом, либо при выполнении некоторых операций ввода или вывода. Однако в ряде случаев полезно иметь более тонкие средства синхронизации, допускающие явное использование по запросу приложения. Блокировка на заданный период времениС помощью метода sleep можно заблокировать задачу на заданный период времени. Мы уже пользовались этим методом в предыдущих приложениях, вызывая его в цикле метода run: try { Thread.sleep(500); } catch (InterruptedException ee) { . . . } В данном примере работа задачи Thread приостанавливается на 500 миллисекунд. Заметим, что во время ожидания приостановленная задача не отнимает ресурсы процессора. Так как метод sleep может создавать исключение InterruptedException, необходимо предусмотреть его обработку. Для этого мы использовали операторы try и catch. Временная приостановка и возобновление работыМетоды suspend и resume позволяют, соответственно, временно приостанавливать и возобновлять работу задачи. Мы уже пользовались этими методами в приложении Rectangles для приостановки и возобновления работы задачи рисования прямоугольников. Задача приостанавливалась, когда курсор мыши оказывался над окном аплета: public boolean mouseEnter(Event evt, int x, int y) { if (m_Rectangles != null) { m_Rectangles.suspend(); } return true; } Работа задачи возобновлялась, когда курсор мыши покидал окно аплета: public boolean mouseExit(Event evt, int x, int y) { if (m_Rectangles != null) { m_Rectangles.resume(); } return true; } Ожидание извещенияЕсли вам нужно организовать взаимодействие задач таким образом, чтобы одна задача управляла работой другой или других задач, вы можете воспользоваться методами wait, notify и notifyAll, определенными в классе Object. Метод wait может использоваться либо с параметром, либо без параметра. Этот метод переводит задачу в состояние ожидания, в котором она будет находиться до тех пор, пока для задачи не будет вызван извещающий метод notify, notifyAll, или пока не истечет период времени, указанный в параметре метода wait. Как пользоваться методами wait, notify и notifyAll? Метод, который будет переводиться в состояние ожидания, должен быть синхронизированным, то есть его следует описать как synchronized: public synchronized void run() { while (true) { . . . try { Thread.wait(); } catch (InterruptedException e) { } } } В этом примере внутри метода run определен цикл, вызывающий метод wait без параметров. Каждый раз при очередном проходе цикла метод run переводится в состояние ожидания до тех пор, пока другая задача не выполнит извещение с помощью метода notify. Ниже мы привели пример задачи, вызывающией метод notify: public void run() { while (true) { try { Thread.sleep(30); } catch (InterruptedException e) { } synchronized(STask) { STask.notify(); } } } Эта задача реализована в рамках отдельного класса, конструктору которого передается ссылка на задачу, вызывающую метод wait. Эта ссылка хранится в поле STask. Обратите внимание, что хотя сам метод run не синхронизированный, вызов метода notify выполняется в синхронизированном режиме. В качестве объекта синхронизации выступает задача, для которой вызывается метод notify. Ожидание завершения задачиС помощью метода join вы можете выполнять ожидание завершения работы задачи, для которой этот метод вызван. Существует три определения метода join: public final void join(); public final void join(long millis); public final void join(long millis, int nanos); Первый из них выполняет ожидание без ограничения во времени, для второго ожидание будет прервано принудительно через millis миллисекунд, а для третьего - через millis миллисекунд и nanos наносекунд. Учтите, что реально вы не сможете указывать время с точностью до наносекунд, так как дискретность системного таймера компьютера намного больше. |