Синхронизация подпроцессов
Основная сложность при написании программ, в которых работают несколько подпроцессов — это согласовать совместную работу подпроцессов с общими ячейками памяти.
Классический пример — банковская транзакция, в которой изменяется остаток на счету клиента с номером numDep. Предположим, что для ее выполнения запрограммированы такие действия:
Deposit myDep = getDeposit(numDep); // Получаем счет с номером numDep
int rest = myDep.getRest(); // Получаем остаток на счету myDep
Deposit newDep = myDep.operate(rest, sum); // Изменяем остаток
// на величину sum
myDep.setDeposit(newDep); // Заносим новый остаток на счет myDep
Пусть на счету лежит 1000 рублей. Мы решили снять со счета 500 рублей, а в это же время поступил почтовый перевод на 1500 рублей. Эти действия выполняют разные подпроцессы, но изменяют они один и тот же счет myDep с номером numDep. Посмотрев еще раз на рис. 17.1 и 17.2, вы поверите, что последовательность действий может сложиться так. Первый подпроцесс проделает вычитание 1000-500, в это время второй подпроцесс выполнит все три действия и запишет на счет 1000+1500 = 2500 рублей, после чего первый подпроцесс выполнит свое последнее действие и у нас на счету окажется 500 рублей. Вряд ли вам понравится такое выполнение двух транзакций.
В языке Java принят выход из этого положения, называемый в теории операционных систем
монитором
(monitor). Он заключается в том, что подпроцесс блокирует объект, с которым работает, чтобы другие подпроцессы не могли обратиться к данному объекту, пока блокировка не будет снята. В нашем примере первый подпроцесс должен вначале заблокировать счет myDep, затем полностью выполнить всю транзакцию и снять блокировку. Второй подпроцесс приостановится и станет ждать, пока блокировка не будет снята, после чего начнет работать с объектом myDep.
Все это делается одним оператором synchronized () {}, как показано ниже:
Deposit myDep = getDeposit(numDep); synchronized(myDep){
int rest = myDep.getRest();
Deposit newDep = myDep.operate(rest, sum);
myDep.setDeposit(newDep);
}
В заголовке оператора synchronized в скобках указывается ссылка на объект, который будет заблокирован перед выполнением блока. Объект будет недоступен для других подпроцессов, пока выполняется блок. После выполнения блока блокировка снимается.
Если при написании какого-нибудь метода оказалось, что в блок synchronized входят все операторы этого метода, то можно просто пометить метод-словом synchronized, сделав его
синхронизированным
(synchronized):
synchronized int getRest()(
// Тело метода
}
synchronized Deposit operate(int rest, int sum) {
// Тело метода
}
synchronized void setDeposit(Deposit dep){
// Тело метода
}
В этом случае блокируется объект, выполняющий метод, т. е. this. Если все методы, к которым не должны одновременно обращаться несколько подпроцессов, помечены synchronized, то оператор synchronized () (} уже не нужен. Теперь, если один подпроцесс выполняет синхронизированный метод объекта, то другие подпроцессы уже не могут обратиться ни к одному синхронизированному методу того же самого объекта.
Приведем простейший пример. Метод run о в листинге 17.5 выводит строку "Hello, World!" с задержкой в 1 секунду между словами. Этот метод выполняется двумя подпроцессами, работающими с одним объектом th. Программа выполняется два раза. Первый раз метод run () не синхронизирован, второй раз синхронизирован, его заголовок показан в листинге 17.4 как комментарий. Результат выполнения программы представлен на рис. 17.3.
Листинг 17.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();
}
}
Рис. 17.3.
Синхронизация метода
Действия, входящие в синхронизированный блок или метод образуют
критический участок
(critical section) программы. Несколько подпроцессов, собирающихся выполнять критический участок, встают в очередь. Это замедляет работу программы, поэтому для быстроты ее выполнения критических участков должно быть как можно меньше, и они должны быть как можно короче.
Многие методы Java 2 SDK синхронизированы. Обратите внимание, что на рис. 17.1 слова выводятся вперемешку, но каждое слово выводится полностью. Это происходит потому, что метод print о класса Printstream синхронизирован, при его выполнении выходной поток system, out блокируется до тех пор, пока метод print () не закончит свою работу.
Итак, мы можем легко организовать последовательный доступ нескольких подпроцессов к полям одного объекта с помощью оператора synchronized () {}. Синхронизация обеспечивает
взаимно исключающее
(mutually exclusive) выполнение подпроцессов. Но что делать, если нужен совместный доступ нескольких подпроцессов к общим объектам? Для этого в Java существует механизм ожидания и уведомления (wait-notify).