Синхронизация потоков
Нередко при многопоточном программировании приходится решать проблему синхронизации потоков. Проблема эта обычно возникает, если разные потоки имеют доступ к одному и тому же ресурсу. Пояснить возникающие при этом сложности можно на следующем примере, не относящемся напрямую к программированию.
Предположим, имеется банковский счет, на который могут вноситься суммы и с которого могут сниматься суммы, причем выполняться могут сразу несколько операций — это обычная ситуация, когда доступ к счету имеют несколько субъектов финансово-экономической деятельности. Процесс изменения состояния счета можно отождествить с потоком. Таким образом, может выполняться сразу несколько потоков.
Непосредственно процесс изменения состояния счета состоит из двух этапов. Сначала сумма, находящаяся на счету, считывается. Затем со считанным значением выполняется нужная операция, после чего новое значение вносится как новое состояние счета. Если в процесс изменения состояния счета между считыванием и записью суммы счета вклинится другой поток, последствия могут быть катастрофическими. Например, пусть есть счет в размере 10 000 рублей. Одним потоком сумма на счету увеличивается на 5000 рублей, а другим — уменьшается на 3000 рублей. Несложно понять, что новое значение счета должно быть равным 12 000 рублей. А теперь проанализируем такую ситуацию. Первым процессом считана сумма в 10 000 рублей. После этого, но до записи первым потоком нового значения, второй поток также считывает сумму на счету. Затем первый поток записывает новое значение счета, то есть 15 000 рублей. После этой оптимистичной процедуры второй поток также записывает свое значение, но это 7000 рублей, поскольку 10 000 – 3000 = 7000. Понятно, что для банка это хорошо, но никак не для обладателя счета. Другой пример: продажа железнодорожных билетов из разных касс. В этом случае из базы данных считывается информация о наличествующих свободных местах, и на одно из них выписывается билет (или не выписывается). Соответствующее место помечается как занятое (то, на которое продан билет). Понятно, что если подобные операции выполняются сразу несколькими потоками, возможны неприятности, поскольку на одни и те же места могут продаваться по несколько билетов, если при выписке билета другой поток успеет считать из базы данных старую информацию, в которой не отражены вносимые при покупке билета изменения. Поэтому при необходимости потоки синхронизируют, что делает невозможным ситуации, подобные описанным.
Существует два способа создания синхронизированного кода:
создание синхронизированных методов;
создание синхронизированных блоков.
В обоих случаях используется ключевое слово synchronized. Если создается синхронизированный метод, ключевое слово synchronized указывается в его сигнатуре. При вызове синхронизированного метода потоком другие потоки на этом методе блокируются — они не смогут его вызвать, пока работу с методом не завершит первый вызвавший его поток. Можно синхронизировать объект в блоке команд. Для этого блок выделяется фигурными скобками, перед которыми указывается ключевое слово synchronized, а в скобках после этого слова — синхронизируемый объект. Пример программы с синхронизированным методом приведен в листинге 5.
Листинг 5.Синхронизация потоков
class MySource{
// Синхронизированный метод:
synchronized void showName(String msg1,String msg2,int time){
try{
// Приостановка потока, из которого вызван метод:
Thread.sleep(time);
// Вывод значения поля msg1:
System.out.print(" Фамилия: "+msg1);
// Еще одна приостановка потока:
Thread.sleep(2*time);
// Вывод значения поля msg2:
System.out.println(" Имя: "+msg2);
}catch(InterruptedException e){// Обработка исключения
System.out.println("Прерывание потока: "+e);}
}}
// Класс, реализующий интерфейс Runnable:
class MakeThread implements Runnable{
// Поле объекта потока:
Thread t;
// Поле-объект MySource:
MySource src;
// Текстовые поля:
String name;
String surname;
int time;
// Конструктор:
MakeThread(String s1,String s2,int time, MySource obj){
surname=s1;
name=s2;
src=obj;
this.time=time;
// Создание потока:
t=new Thread(this);
// Запуск потока:
t.start();}
// Определение метода run():
public void run(){
src.showName(surname,name,time);}
}
class SynchThreads{
public static void main(String args[]){
// Объект "ресурса":
MySource obj=new MySource();
// Создание потоков:
MakeThread fellow1=new MakeThread("Иванов","Иван",1000,obj);
MakeThread fellow2=new MakeThread("Петров","Петр",450,obj);
MakeThread fellow3=new MakeThread("Сидоров","Сидор",1450,obj);
try{ // Ожидать завершения потоков
fellow1.t.join();
fellow2.t.join();
fellow3.t.join();
}catch(InterruptedException e){ // Обработка исключения
System.out.println("Прерывание потока: "+e);}
}}
Идея, положенная в основу алгоритма программы, достаточно проста. В главном потоке создаются и запускаются три дочерних потока, и каждый из них с временной задержкой выводит два сообщения: одно с именем и другие с фамилией. Однако для вывода сообщений используется один и тот же объект класса MySource, а точнее, метод showName этого объекта. Таким образом, три потока в процессе своего выполнения в разное время обращаются к одному и тому же объекту, который играет роль общего ресурса. Этот объект создается в главном методе программы main() в классе SynchThreads с помощью команды MySource obj=new MySource(). Описание класса MySource можно найти в начале листинга 5. В этом классе описан всего один метод showName(), причем метод синхронизирован — об этом свидетельствует инструкция synchronized в сигнатуре метода. У метода три аргумента: два текстовых и один целочисленный.
Текстовые аргументы определяют фамилию и имя виртуального пользователя, а третий числовой аргумент определяет значение задержки между выводимыми методом сообщениями. В частности, перед выводом первого сообщения задержка равна, в миллисекундах, значению третьего аргумента, а интервал между первым и вторым сообщениями в два раза больше. Для приостановки выполнения потока используется статический метод sleep(). Также в методе showName() обрабатывается исключение класса InterruptedException — «прерывание потока».
Потоки создаются с помощью класса MakeThread, реализующего интерфейс Runnable. У класса пять полей: поле t класса Thread, на котором реализуется поток, объектная ссылка src класса MySource, два текстовых поля name и surname и целочисленное поле time. Поле через объектную ссылку src ссылается на внешний объект класса MySource, посредством которого осуществляется вывод информации на экран. В поле name записывается имя, а в поле surname — фамилия виртуального пользователя. Целочисленный аргумент time содержит значение базовой задержки вывода сообщений. Конструктор класса MakeThread имеет четыре аргумента, которыми задаются значения полей класса surname, name, time и src соответственно. Командой t=new Thread(this) создается объект для потока, а командой t.start() поток запускается.
В методе run() командой src.showName(surname,name,time) из объекта src запускается метод showName(). Аргументами методу передаются значения полей объекта класса MakeThread, из которого запускается поток.
В главном методе программы, кроме объекта obj, создаются три объекта fellow1, fellow2 и fellow3 класса MakeThread (с разными аргументами). Затем в главном потоке с помощью метода join(), который вызывается из объекта-поля t каждого из трех объектов fellow1, fellow2 и fellow3, дается указание ожидать окончания выполнения каждого из трех потоков. В результате выполнения программы
получаем:
Фамилия: Иванов Имя: Иван
Фамилия: Сидоров Имя: Сидор
Фамилия: Петров Имя: Петр
Таким образом, имеет место соответствие между фамилиями и именами виртуальных пользователей. Такое соответствие достигается благодаря синхронизации метода showName(). Убедиться в последнем просто — достаточно убрать ключевое слово synchronized из сигнатуры метода showName(). Результат выполнения программы после этого изменится радикально:
Фамилия: Петров Фамилия: Иванов Имя: Петр
Фамилия: Сидоров Имя: Иван
Имя: Сидор
Проблема в том, что каждый поток из-за задержки перед выводом сообщений какое-то время работает с объектом obj, через который выводятся сообщения.
При отсутствии синхронизации в работу одного потока вклинивается другой поток, в результате сообщения появляются достаточно хаотично (хотя по строгой логической схеме, в соответствии с программным кодом).
В завершение отметим, что реализовать синхронизацию общего ресурса в данном случае можно было и по-другому. Например:
public void run(){
src.showName(surname,name,time);
}
Вместо подобной реализации метода run() в классе MakeThread можно было сделать следующее:
public void run(){
synchronized(src){
src.showName(surname,name,time);
}
}
В данном случае синхронизируемый код выделен в блок, а перед ним в скобках после ключевого слова synchronized указан синхронизируемый объект src.
Дата добавления: 2016-06-22; просмотров: 2004;