Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
ГЛАВА Многопоточное программирование В отличие от некоторых языков программирования, Java предлагает встроенную поддержку многопоточного программирования М ногопоточная программа содержит две или более частей, которые могут выполняться одновременно. Каждая часть такой программы называется потоком (thread), и каждый поток задает отдельный путь выполнения. Другими словами, многопоточность — это специализированная форма многозадачности. Вы наверняка знакомы с многозадачностью, поскольку она поддерживается практически всеми современными операционными системами. Однако существует два отдельных типа многозадачности многозадачность, основанная на процессах, и многозадачность, основанная на потоках. Важно понимать разницу между ними. Большинству читателей более знакома многозадачность, основанная на процессах. Процесс по сути своей — это выполняющаяся программа. То есть многозадачность, основанная на процессах, представляет собой средство, которое позволяет вашему компьютеру одновременно выполнять две или более программ. Так, например, основанная на процессах многозадачность позволяет запускать компилятор Java в то самое время, когда вы используете текстовый редактор или посещаете вебсайт. В многозадачности, основанной на процессах, программа представляет собой наименьший элемент кода, которым может управлять планировщик операционной системы. В среде поточной многозадачности наименьшим элементом управляемого кода является поток. Это означает, что одна программа может выполнять две или более задач одновременно. Например, текстовый редактор может форматировать текст в тоже время, когда выполняется его печать, — до тех пор, пока эти два действия выполняются двумя отдельными потоками. То есть многозадачность на основе процессов имеет дело с картиной в целом, а потоковая многозадачность справляется с деталями. Многозадачные потоки требуют меньше накладных расходов, чем многозадачные процессы. Процессы — это тяжеловесные задачи, каждая из которых требует собственного адресного пространства. Межпроцессные коммуникации дорогостоящи и ограничены. Переключение контекста от одного процесса к другому также обходится дорого. С другой стороны, потоки проще. Они совместно используют одно и тоже адресное пространство и один и тот же тяжеловесный процесс. Коммуникации между потоками экономны, а переключения контекста между потоками характеризуются низкой стоимостью. Хотя программы Java используются в многозадачных средах на основании процессов, такая многозадачность средствами Java не контролируется. А вот многопоточная многозадачность средствами Java контролируется. Многопоточность позволяет вам писать эффективные программы, которые по максимуму используют доступную мощь процессора системы. Еще одним преимуществом многопоточности является сведение к минимуму времени ожидания. Это особенно важно для интерактивных сетевых сред, в которых работает Java, так как в них наличие ожидания и простоев — обычное явление. Например, скорость переда 2 6 0 Часть I. Язык Java чи данных посети намного ниже, чем скорость, с которой компьютер может их обрабатывать. Даже чтение и запись ресурсов локальной файловой системы намного медленнее, чем темп их обработки в процессоре. И, конечно, пользователь намного медленнее вводит данные с клавиатуры, чем их может обработать компьютер. В однопоточных средах ваша программа вынуждена ожидать окончания таких задач, прежде чем переходить к следующей, — даже если большую часть времени программа простаивает, ожидая ввода. Многопоточность помогает сократить время простоя, поскольку другие потоки могут выполняться, пока один ожидает. Если вы программировали для таких операционных систем, как Windows, это значит, что вы уже знакомы с многопоточным программированием. Однако тот факт, что Java управляет потоками, делает многопоточность особенно удобной, поскольку многие детали подконтрольны вам как программисту. Модель потоков Система времени выполнения Java зависит от потоков во многих отношениях, и все библиотеки классов спроектированы с учетом многопоточности. Фактически Java использует потоки для того, чтобы обеспечить асинхронность всей среде выполнения. Это позволяет снизить неэффективность за счет предотвращения бесполезной растраты циклов центрального процессора. Значение многопоточной среды лучше понимается при сравнении. Однопоточные системы используют подход, называемый циклом событий с опросом В этой модели единственный поток управления выполняется в бесконечном цикле, опрашивая единственную очередь событий, чтобы принять решение о том, что делать дальше. Как только этот механизм опроса возвращает, скажем, сигнал о том, что сетевой файл готов к чтению, цикл событий передает управление соответствующему обработчику событий. До тех пор, пока тот не вернет управление, в программе ничего не может произойти. Это расходует время процессора. Это также может привести к тому, что одна часть программы будет доминировать над другими и не позволять обрабатывать любые другие события. Вообще говоря, в однопоточном окружении, когда поток блокируется (то есть приостанавливает выполнение) по причине ожидания некоторого ресурса, выполнение всей программы приостанавливается. Выгода от многопоточности состоит в том, что основной механизм циклического опроса исключается. Один поток может быть приостановлен без остановки других частей программы. Например, время ожидания при чтении данных из сети либо ожидание пользовательского ввода может быть утилизировано где угодно. М ногопоточность позволяет циклам анимации засыпать на секунду между показом соседних кадров, не приостанавливая работы всей системы. Когда поток блокируется в программе Java, то останавливается только один-единственный заблокированный поток. Все остальные потоки продолжают выполняться. За последние несколько лет многоядерные системы стали вполне обычным явлением. Конечно, одноядерные системы все еще широко распространены и используются. Важно понимать, что многопоточные средства Java работают в обоих типах систем. В одноядерной системе одновременно выполняющиеся потоки совместно используют процессор, получая для каждого потока некий сектор процессорного времени. Поэтому в одноядерной системе два или более потоков фактически не выполняются одновременно, они ожидают своей очереди на использование процессорного времени. Нов многоядерных системах два или более потоков фактически могут выполняться одновременно. Во многих случаях это может увеличить эффективность программы и повысить скорость определенных операций Глава 11. Многопоточное программирование 6 На заметку В JDK 7 добавлена инфраструктура Fork/Join Framework — мощное средство создания многопоточных приложений с автоматическим масштабированием для лучшего использования многоядерных систем. Инфраструктура Fork/Join Framework — это часть поддержки Java для параллельного программирования, которое предоставляет технологии оптимизации некоторых типов алгоритмов параллельного выполнения в системах с несколькими процессорами Более подробная информация об Fork/Join Framework и других утилитах параллельности приведена в главе 27, а здесь рассматриваются традиционные многопоточные возможности Потоки существуют в нескольких состояниях. Вот их общее описание. Поток может выполняться Он может быть готов к выполнению, как только получит время центрального процессора. Работающий поток может быть приостановлен, что временно прекращает его активность. Выполнение приостановленного потока может быть возобновлено, позволяя ему продолжить работу с того места, где он был приостановлен. Поток может быть заблокирован, когда ожидает какого-то ресурса. В любой момент поток может быть прерван, что немедленно останавливает его выполнение. Однажды прерванный поток уже не может быть возобновлен. Приоритеты потоков присваивает каждому потоку приоритет, который определяет поведение данного потока по отношению к другим. Приоритеты потоков задаются целыми числами, определяющими относительный приоритет одного потока по сравнению с другими. Значение приоритета само по себе никакого смысла не имеет — более высокоприоритетный поток не выполняется быстрее, чем низкоприоритетный, когда он является единственным выполняемым потоком в данный момент. Вместо этого приоритет потока используется для принятия решения при переключении от одного выполняющегося потока к другому. Это называется переключением контекста Правила, которые определяют, когда должно происходить переключение контекста, достаточно просты. • Поток может добровольно уступить управление Для этого можно явно уступить очередь выполнения, приостановить поток или блокировать на время ожидания ввода-вывода. При таком сценарии все прочие потоки проверяются, и ресурсы процессора передаются потоку с максимальным приоритетом, который готов к выполнению. • Поток может быть прерван другим, более приоритетным потоком В этом случае низкоприоритетный поток, который не занимает процессор, просто приостанавливается высокоприоритетным потоком, независимо оттого, что он делает. В основном, высокоприоритетный поток выполняется, как только он этого захочет. Это называется вытесняющей многозадачностью (или многозадачностью с приоритетами). В случае, когда два потока, имеющие одинаковый приоритет, претендуют на цикл процессора, ситуация усложняется. Для таких операционных систем, как Windows, потоки с одинаковым приоритетом разделяют время в циклическом режиме. Для операционных систем других типов потоки с одинаковым приоритетом должны принудительно передавать управление своим родственникам. Если они этого не делают, другие потоки не запускаются. Вним ание! Из-за разницы в способах переключения операционными системами потоковых контекстов могут возникать проблемы переносимости Синхронизация Поскольку многопоточность дает вашим программам возможность асинхронного поведения, должен существовать способ обеспечить синхронизацию, когда в этом возникает необходимость. Например, если вы хотите, чтобы два потока взаимодействовали и совместно использовали сложную структуру данных, такую как связный список, то вынуждаетесь в способе предотвращения конфликтов между ними. То есть следует предотвратить запись данных водном потоке, когда другой занимается их чтением. Для этой цели в Java реализован элегантный трюк из старой модели межпроцессной синхронизации, а именно — монитор Монитор это управляющий механизм, впервые реализованный Чарльзом Энтони Ричардом Хоаром. Вы можете воспринимать монитор как очень маленький ящик, который принимает только один поток в единицу времени. Как только поток вошел в монитор, все другие потоки должны ждать, пока тот не покинет его. Таким образом, монитор может быть использован для защиты совместно используемых ресурсов от одновременного использования более чем одним потоком. Большинство многопоточных систем применяет мониторы как объекты, которые ваша программа может получить и которыми она может манипулировать. Java предлагает более чистое решение. Не существует отдельного класса монитора вроде “M onitor”. Вместо этого каждый объект имеет собственный неявный монитор, вход в который осуществляется автоматически, когда вызывается синхронизированный метод объекта. Когда поток находится внутри синхронизированного метода, ни один другой поток не может вызвать никакого синхронизированного метода этого объекта. Это позволяет вам писать очень ясный и краткий многопо точный код, поскольку поддержка синхронизации встроена в язык. Обмен сообщениями После того как вы разделите программу на отдельные потоки, вам нужно определить, как они будут общаться друг с другом. При программировании на некоторых других языках для установки взаимодействия между потоками вы вынуждены зависеть от операционной системы. То есть, конечно же, появляются накладные расходы. В отличие от этих языков, Java предоставляет ясный и экономичный способ общения двух или более потоков между собой — за счет вызова предопределенных методов, которыми обладают объекты. Система сообщений Java позволяет потоку войти в синхронизированный метод объекта и ожидать, пока какой-то другой поток явно не уведомит его о прибытии. Класс T h r e a d и интерфейс R u n n a b l М ногопоточная система Java встроена в класс Thread, его методы и дополняющий его интерфейс Runnable. Класс Thread инкапсулирует поток выполнения. Поскольку вы не можете напрямую обратиться к нематериальному состоянию работающего потока, имеете дело сего заместителем (proxy) — экземпляром класса Thread, который породил его. Чтобы создать новый поток, ваша программа должна либо расширить класс Thread, либо реализовать интерфейс Класс Thread определяет несколько методов, которые помогают управлять потоками. Некоторые из них, которые будут упомянуты в настоящей главе, перечислены в табл. 11.1. 2 6 2 Часть I. Язык Java Глава 11. Многопоточное программирование 2 6 3 Таблица 11.1. Методы управления потоками класса T h r e a d Назначение getName getPriority isAlive join run sleep start Получить имя потока Получить приоритет потока Определить, выполняется ли поток Ожидать завершения потока Входная точка потока Приостановить выполнение потока на заданное время Запустить поток вызовом его метода run (До сих пор во всех примерах книги использовался единственный поток управления. Далее в этой главе объясняется, как применять класс Thread и интерфейс Runnable для создания потоков и управления ими, начиная с потока, который есть в каждой программе Java, — главного. Когда программа Java стартует, немедленно начинает выполняться один поток. Обычно его называют главным потоком (main thread) программы, потому что это тот поток, который запускается вместе с вашей программой. Главный поток важен по двум причинам. • Это поток, от которого порождаются все дочерние потоки. • Часто он должен быть последним потоком, завершающим выполнение, так как предпринимает различные завершающие действия. Несмотря на то что главный поток создается автоматически при запуске программы, им можно управлять через объект класса Thread. Для этого следует получить ссылку на него вызовом метода currentThread () , который является открытым статическим (public static) методом класса Thread. Его общая форма выглядит следующим образом Thread Этот метод возвращает ссылку на поток, из которого он был вызван. Получив ссылку на главный поток, вы можете управлять им точно также, как любым другим. Рассмотрим следующий пример Управление главным потоком class CurrentThreadDemo { public static void main(String a r g s []) { Thread t = Текущий поток " + t); // изменить имя потока t . setName (Мой Thread11) После изменения имени " + t); try { for(int n = 5; n > 0; n--) { System.out.println(n); Thread.sleep(1000); } } catch (InterruptedException e) Главный поток 2 6 Часть I. Язык Главный поток прерван"); } } } В этой программе ссылка на текущий поток (в данном случае — главный) получается вызовом метода current Thread () , и эта ссылка сохраняется в локальной переменной t. Далее программа отображает информацию о потоке. Программа вызывает метод setName () для изменения внутреннего имени потока. После этого информация о потоке отображается заново. Далее в цикле выводятся цифры в обратном порядке с задержкой на 1 секунду после каждой строки. Пауза организуется вызовом метода sleep (). Аргумент метода sleep () задает период задержки в миллисекундах. Обратите внимание на блок try /catch вокруг цикла. Метод sleep () в классе Thread может передать исключение InterruptException. Это может произойти, если некоторый другой поток захочет прервать выполнение этого спящего потока. Данный пример просто выводит сообщение, если поток прерывается. В реальных программах выбудете обрабатывать подобную ситуацию иначе. Ниже показан вывод, создаваемый этой программой. Текущий поток Thread[main,5 После изменения имени Thread[My Thread,5,main] 5 4 3 Обратите внимание на то, что вывод создается, когда переменная t используется в качестве аргумента для метода print In () . Он отображает по порядку имя потока, его приоритет и имя его группы. По умолчанию имя главного потока — m ain. Его приоритет равен 5, что является значением по умолчанию, a main — также имя группы потоков, к которой относится данный. Группа потоков — это структура данных, которая управляет состоянием набора потоков в целом. После того как имя потока изменено, переменная t выводится вновь. На этот раз отображается новое имя потока. Давайте поближе взглянем на определенные в классе Thread методы, которые используются в программе. Метод sleep () заставляет поток, из которого он был вызван, приостановить выполнение на указанное количество миллисекунд. Его общая форма выглядит так void sleep(long миллисекунды throws InterruptedException Количество миллисекунд, на которое нужно приостановить выполнение, передается в параметре миллисекунды Этот метод может передать исключение Метод sleep () имеет также вторую форму, показанную ниже, которая позволяет задать период в миллисекундах и наносекундах void sleep(long миллисекунды long наносекунды throws InterruptedExcept Вторая форма может применяться только в средах, которые предусматривают задание временных периодов в наносекундах. Как показано в предыдущей программе, вы можете установить имя потока, используя метод setName () . Получить имя потока можно вызовом метода g e t - Name () (эта процедура в программе не показана. Эти методы являются членами класса Thread и объявлены следующим образом, final void setName(String имя_потока) final String Здесь имя_потока указывает имя потока Глава 11. Многопоточное программирование 6 Создание потока В наиболее общем смысле вы создаете поток, реализуя объект класса Thread. В Java для этого определены два способа. • С помощью реализации интерфейса С помощью расширения класса В следующих разделах рассматриваются эти способы по очереди. |