Синхронизация подпроцессов
Синхронизация подпроцессов
Основная сложность при написании программ, в которых работают несколько подпроцессов — это согласовать совместную работу подпроцессов с общими ячейками памяти.
Классический пример — банковская транзакция, в которой изменяется остаток на счету клиента с номером 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.