К. Арнольд, Д. Гослинг - Язык программирования Java (1160779), страница 35
Текст из файла (страница 35)
Прииспользовании приведенной выше схемы “выборка-изменение-запись” возникает потенциальная опасность того, что при одновременнойработе двух потоков с одним и тем же объектом произойдет наложение, приводящее к разрушению объекта. Давайте представим, что в нашемпримере с банком некто желает внести средства на счет.Почти одновременно второй клиент приказывает другому работнику банка положить деньги на тот же самый счет. Оба работника идут в архив,чтобы найти информацию о счете (были же времена, когда в банках использовались бумажные картотеки!) и получают одинаковые данные.
Затемони возвращаются к своим столам, заносят требуемую сумму на счет и идут обратно в архив, чтобы записать свои результаты, полученныенезависимо друг от друга. В таком случае на состоянии счета отразится лишь последняя из записанных транзакций; первая транзакция будетпопросту потеряна.В настоящих банках проблема решалась просто: работник оставлял в папке записку “Занято; подождите завершения работы”. В компьютерепроисходит практически то же самое: с объектом связывается понятие блокировка (lock), по которой можно определить, используется объект илинет.Многие реальные задачи программирования лучше всего решаются с применением нескольких потоков.
Например, интерактивные программы,предназначенные для графического отображения данных, нередко разрешают пользователю изменять параметры отображения в реальномвремени. Оптимальное динамическое поведение интерактивных программ достигается благодаря использованию потоков. В однопоточныхсистемах иллюзия работы с несколькими потоками обычно достигается за счет использования прерываний или программных запросов (polling).Программные запросы служат для объединения частей приложения, управляющих отображением информации и вводом данных.
Особеннотщательно должна быть написана программа отображения — запросы от нее должны поступать достаточно часто, чтобы реагировать на вводинформации пользователем в течение долей секунды. Эта программа либо должна позаботиться о том, чтобы операции графического выводазанимали минимальное время, либо прерывать свою собственную работу для выполнения запросов. Такое смешение двух разнородныхаспектов программы приводит к появлению сложного, а порой и нежизнеспособного кода.С указанными проблемами проще всего справиться в многопоточной системе.
Один поток обновляет изображение на основе текущих данных, адругой — обрабатывает ввод со стороны пользователя. Если ввод оказывается сложным (например, пользователь заполняет экранную форму),первый поток (вывод данных) может работать независимо, вплоть до получения новой информации. В модели с применением программныхзапросов приходится либо приостанавливать обновление изображения, чтобы дождаться завершения нетривиального ввода, либо производитьсложную синхронизацию, чтобы изображение могло обновляться во время заполнения формы пользователем. Модель с разделением процессовввода и отображения может поддерживаться в многопоточной системе непосредственно, вместо того чтобы заново подгонять ее дляреализации очередной задачи.9.1. Создание потоковПотоки, как и строки, представлены классом в стандартных библиотеках Java.
Чтобы породить новый поток выполнения, для начала следуетсоздать объект Thread:Thread worker = new Thread();После того как объект-поток будет создан, вы можете задать его конфигурацию и запустить. В понятие конфигурации потока входит указаниеисходного приоритета, имени и так далее. Когда поток готов к работе, следует вызвать его метод start. Метод start порождает новыйвыполняемый поток на основе данных объекта класса Thread, после чего завершается.
Метод start вызывает метод run нового потока, чтоприводит к активизации последнего.Выход из метода run означает прекращение работы потока. Поток можно завершить и явно, посредством вызова stop; его выполнение можетбыть приостановлено методом suspend; существуют много других средств для работы с потоками, которые мы вскоре рассмотрим.Стандартная реализация Thread.run не делает ничего. Вы должны либо расширить класс Thread, чтобы включить в него новый метод run, либосоздать объект Runnable и передать его конструктору потока.
Сначала мы рассмотрим процесс порождения новых потоков за счет расширенияThread, а позже займемся техникой работы с Runnable (см. “Использование Runnable”).Приведенная ниже простая программа задействует два потока, которые выводят слова “ping” и “PONG” с различной частотой:class PingPong extends Thread {String word;// выводимое словоint delay;// длительность паузыPingPong(String whatToSay, int delayTime) {word = whatToSay;delay = delayTime;}public void run() {try {for (;;) {System.out.print(word + " ");sleep(delay);// подождать следующего вывода}} catch (InterruptedException e) {return;}}public static void main(String[] args) {new PingPong("ping", 33).start(); // 1/30 секундыnew PingPong("PONG", 100).start(); // 1/10 секунды}}Мы определили тип потока с именем PingPong.
Его метод run работает в бесконечном цикле, выводя содержимое поля word и делая паузу наdelay микросекунд. Метод PingPong.run не может возбуждать исключений, поскольку этого не делает переопределяемый им метод Thread.run.Соответственно, мы должны перехватить исключение InterruptedException, которое может возбуждаться методом sleep.После этого можно непосредственно создать выполняющиеся потоки — именно это и делает метод PingPong.
Он конструирует два объектаPingPong, каждый из которых обладает своим выводимым словом и интервалом задержки, после чего вызывает методы start обоих объектовпотоков. С этого момента и начинается работа потоков. Примерный результат работы может выглядеть следующим образом:pingpingpingpingPONGpingPONGPONGpingpingpingpingpingpingPONGpingpingPONGpingpingpingPONGpingpingPONGpingpingpingPONGpingpingPONGpingpingpingPONGpingpingPONGpingpingpingpingpingpingPONGpingpingPONGPONGpingpingPONGpingpingpingPONGpingpingPONG ...Поток может обладать именем, которое передается в виде параметра типа String либо конструктору, либо методу setName.
Вы получите текущееимя потока, если вызовете метод getName. Имена потоков предусмотрены исключительно для удобства программиста — в системе runtime вJava они не используются.Вызов статического метода Thread.currentThread позволяет получить объект Thread, который соответствует работающему в настоящий моментпотоку.9.2. СинхронизацияВспомним пример со служащими банка, о которых мы говорили в начале главы. Когда два работника (потока) должны воспользоваться одной итой же папкой (объектом), возникает опасность, что наложение операций приведет к разрушению данных. Работники банка синхронизируютсвой доступ с помощью записок.
Эквивалентом такой записки в условиях многопоточности является блокировка объекта. Когда объектзаблокирован некоторым потоком, только этот поток может работать с ним.9.2.1. Методы synchronizedЧтобы класс мог использоваться в многопоточной среде, необходимо объявить соответствующие методы с атрибутом synchronized (позднее мыузнаем, что же входит в понятие “соответствующие”).
Если некоторый поток вызывает метод synchronized, то происходит блокировка объекта.Вызов метода synchronized того же объекта другим потоком будет приостановлен до снятия блокировки.Синхронизация приводит к тому, что выполнение двух потоков становится взаимно исключающим по времени. Проблема вложенных вызововрешается очевидным образом: если синхронизированный метод вызывается для объекта, который ранее был заблокирован тем же самымпотоком, то метод выполняется, однако блокировка не снимается вплоть до выхода из самого внешнего синхронизированного метода.Синхронизация решает проблему, возникающую в нашем примере: если действия выполняются в синхронизированном методе, то при попыткеобращения к объекту со стороны второго потока в тот момент, когда с объектом работает первый поток, доступ будет отложен до снятияблокировки.Приведем пример того, как мог бы выглядеть класс Account, спроектированный для работы в многопоточной среде:class Account {private double balance;public Account(double initialDeposit) {balance = initialDeposit;}public synchronized double getBalance() {return balance;}public synchronized void deposit(double amount) {balance += amount;}}А теперь мы объясним, что же означает понятие “соответствующие” применительно к синхронизированным методам.Конструктор не обязан быть synchronized, поскольку он выполняется только при создании объекта, а это может происходить только в одномпотоке для каждого вновь создаваемого объекта.