Поточная модель Java
Многопоточное программирование
Язык Java поддерживает многопоточное программирование. Многопоточная программа содержит две или больше частей, которые могут выполняться одновременно. Каждая такая часть программы называется потоком.
С понятием многопоточности тесно связано понятие многозадачности. Многозадачность реализуется, как правило, либо на потоках, либо на процессах. Различие между потоками и процессами достаточно зыбкое. Обычно для процессов выделяется отдельная область памяти, которая доступна только для этих процессов. Это повышает безопасность, но снижает скорость выполнения программы.
На процессах основана работа операционных систем.
При многопоточности обычно память по потокам не разбивается. Хотя такая ситуация может сказаться на стабильности программы, системные ресурсы используются экономнее и программа работает быстрее.
Обычно многопоточное программирование применяют для сведения к минимуму времени простоя системы, поскольку сразу несколько задач могут выполняться одновременно.
Поточная модель Java
При однопоточном программировании в бесконечном цикле выполняется один поток управления, который опрашивает единую очередь событий и принимает решение, какое действие выполнять следующим. Примером может быть процесс считывания информации из файла. После получения сигнала о готовности файла к считыванию управление передается соответствующему обработчику, и пока из этого обработчика не будет получен ответ, никаких новых действий не предпринимается. В противовес этому при многопоточном программировании один поток может делать паузу, не прерывая выполнение других потоков.
Именно такой подход реализован в Java.
Как и все в Java, поточная модель реализуется посредством иерархии классов, описывающих потоки. Основу этой иерархии составляют класс Thread и интерфейс Runnable. Для создания потока необходимо либо расширить класс Thread, либо реализовать интерфейс Runnable. При этом класс Thread инкапсулирует поток исполнения.
При запуске Java-программы начинает выполняться главный поток. Особенность главного потока состоит в том, что в нем порождаются все дочерние потоки. Главный поток отождествляется с программой. Программа начинается с выполнения главного потока и должна завершаться с завершением главного потока.
В отличие от дочерних потоков главный поток создается автоматически. Поэтому в предыдущих примерах никаких дополнительных действий для создания главного потока не применялось.
Главным потоком можно управлять. Делается это с помощью поточного объекта с использованием методов класса Thread. Некоторые из этих методов приведены в табл.1.
Таблица 1.Методы класса Thread
Метод | Описание |
currentThread() | Методом в качестве результата возвращается ссылка на поток, из которого вызывается метод |
getName() | Метод в качестве результата возвращает имя потока (текстовую строку) |
getPriority() | Метод в качестве результата возвращает приоритет потока (целое число) |
isAlive() | Метод позволяет выяснить, используется поток или нет |
join() | Методом дается команда ожидания завершения потока |
run() | Метод определения точки входа в поток |
sleep() | Метод для приостановки потока на определенный промежуток времени (аргумент метода, в миллисекундах) |
start() | Метод для запуска потока путем вызова его метода run() |
В частности, ссылку на поток получают с помощью метода currentThread(), который является членом класса Thread и имеет атрибуты public и static. Метод имеет сигнатуру public static Thread currenThread(). Пример использования метода currentThread() для получения доступа к главному потоку приведен в листинге 1.
Листинг 1.Главный поток программы
class CurrentThreadDemo{
public static void main(String args[]){
// Объектная переменная t класса Thread:
Thread t;
// Объектная переменная t ссылается на главный поток программы:
t=Thread.currentThread();
// Информация о потоке:
System.out.println("Активный поток: "+t);
// Потоку присвоено (изменено) имя:
t.setName("Самый главный поток");
// Информация о потоке:
System.out.println("После изменения имени: "+t);
try{
for(int n=5;n>0;n--){
System.out.println(n);
// Приостановка потока:
Thread.sleep(1000);}
}catch(InterruptedException e){ // Обработка исключения "прерывание потока"
System.out.println("Поток завершен!");}}
}
В главном методе программы командой Thread t объявляется объектная переменная t класса Thread. Значение этой переменной, то есть ссылка на поток, присваивается командой t=Thread.currentThread() (кстати, можно было две команды объединить в одну:
Thread t=Thread.currentThread()). Поскольку метод currentThread() статический, его можно вызывать, не создавая объект, а указав имя класса, что, собственно, и было сделано. В качестве значения метод возвращает ссылку на тот поток, из которого метод вызывался. В данном случае это главный поток программы. В результате в объектную переменную t записывается ссылка на главный поток программы. Теперь, если нам понадобится обратиться к главному потоку, мы можем воспользоваться переменной t. Хочется обратить внимание читателя на уже упоминавшийся факт: главный поток создается автоматически. Он существует безотносительно того, объявляем мы переменную t или нет. Эта переменная нужна лишь для того, чтобы идентифицировать поток, так сказать, поймать его «за уши».
Командой System.out.println("Активный поток: "+t) на экран выводится информация о главном потоке. Объект t, переданный аргументом методу println(), приводится к текстовому формату (благодаря переопределенному для класса Thread методу toString()). В результате на экран выводится сообщение:
Активный поток: Thread[main,5,main]
Часть сообщения Thread[main,5,main] является результатом приведения объекта потока к текстовому формату. В квадратных скобках после ключевого слова Thread соответственно указываются: имя потока, приоритет и группа потока.
Для главного потока по умолчанию именем является main, приоритет равен 5, а поток относится к группе с именем main. Имя потока — это его уникальный текстовый идентификатор. Приоритет — целое число. Само значение приоритета особой важности не имеет, важно только, у какого потока оно больше.
Приоритет определяет, какому потоку отдается предпочтение при выполнении программы, когда один поток прерывается другим. Поток с низким приоритетом может быть остановлен потоком с более высоким приоритетом. Все потоки разбиваются на группы. Приоритеты потоков сравниваются в пределах групп.
Командой t.setName("Самый главный поток") меняется имя главного потока. Новое имя потока "Самый главный поток" указывается аргументом метода setName(), который вызывается из объекта главного потока. Поэтому после выполнения команды System.out.println("После изменения имени: "+t) на экране появляется сообщение:
После изменения имени: Thread[Самый главный поток,5,main]
Далее в рамках цикла на экран в столбик выводятся цифры от 5 до 1. При этом использована команда Thread.sleep(1000) для приостановки потока в каждом цикле. В результате выполнения программы получаем:
Активный поток: Thread[main,5,main]
После изменения имени: Thread[Самый главный поток,5,main]
Причем цифровой ряд выводится на экран с заметной задержкой (порядка одной секунды).
Поскольку метод sleep() может выбрасывать исключение InterruptedException (пре рывание потока) и это исключение неконтролируемое, то в программе предусмотрена его обработка. В противном случае пришлось бы отразить в сигнатуре метода main() тот факт, что он выбрасывает исключение класса InterruptedException.
Создание потока
Как уже отмечалось, для создания потока (кроме главного) следует либо расширить класс Thread, либо реализовать интерфейс Runnable. Сначала рассмотрим создание потока путем реализации интерфейса Runnable.
Создать поток можно на базе любого класса, который реализует интерфейс Runnable. При реализации интерфейса Runnable достаточно определить всего один метод run(). Программный код этого метода — это тот код, который выполняется в рамках создаваемого потока. Говорят, что метод run() определяет точку входа в поток. Метод run() имеет следующую сигнатуру:
public void run()
Для начала выполнения потока вызывают метод start().
Общая последовательность действий при создании нового потока путем реализации интерфейса Runnable следующая:
Определяется класс, реализующий интерфейс 1. Runnable. В этом классе определяется метод run(). В этом классе создается объект класса 2. Thread. Конструктору класса передается два аргумента: объект класса, реализующего интерфейс Runnable, и текстовая строка — название потока. Для запуска потока из объекта класса 3. Thread вызывается метод start().
Другими словами, для того чтобы определить программный код, выполняемый в новом потоке, необходимо расширить интерфейс Runnable, причем указанный код потока — это, фактически, код метода run(), определяемого в классе, расширяющем интерфейс Runnable. Поток в Java — это объект класса Thread. Поэтому для создания потока необходимо создать объект этого класса. В то же время при создании потока необходимо указать код этого потока (то есть код соответствующего метода run()). Код потока определяется в классе, реализующем интерфейс Runnable. Объект этого класса передается аргументом конструктору класса Thread при создании объекта нового потока. Поскольку создание потока не означает его запуск, поток запускается с помощью метода start(), вызываемого из объекта потока (объект класса Thread).
Часто процесс создания нового потока реализуется по следующей схеме:
При расширении интерфейса 1. Runnable в соответствующем классе (для удобства назовем его внешним) не только определяется метод run(), но и описывается поле — объект класса Thread. Создание объекта класса 2. Thread (объекта потока), ссылка на который присваивается полю Thread, выполняется в конструкторе внешнего класса. При создании этого объекта вызывается конструктор класса Thread, первым аргументом которому передается ссылка this. Таким образом, одновременно с созданием объекта внешнего класса создается и объект потока, причем объект потока создается на основе объекта внешнего класса. В конструкторе внешнего класса после команды создания объекта потока 3 (объекта класса Thread) из этого потока вызывается метод start(). Это приводит к запуску потока. Для создания и запуска потока в главном методе программы создается объект описанного ранее класса, расширяющего интерфейс Runnable. Поскольку для запуска потока достаточно самого факта создания объекта, нередко этот создаваемый объект является анонимным, то есть ссылка на него ни в какие объектные переменные не записывается.
Пример создания потока на основе реализации интерфейса Runnable приведен в листинге 2.
Листинг 2.Создание потока реализацией интерфейса Runnable
// Класс, расширяющий интерфейс Runnable:
class NewThread implements Runnable{
// Поле - ссылка на объект потока:
Thread t;
// Конструктор класса:
NewThread(){
// Создание объекта потока:
t=new Thread(this,"Новый поток");
// Вывод сведений о потоке:
System.out.println("Дочерний поток: "+t);
t.start(); // Запуск потока
}
// Определение метода run():
public void run(){
try{
for(int i=5;i>0;i--){
System.out.println("Дочерний поток: "+i);
// Приостановка потока:
Thread.sleep(500);}
}
// Обработка исключительной ситуации прерывания потока
catch(InterruptedException e){
System.out.println("Прерывание дочернего потока!");}
System.out.println("Завершение дочернего потока!");
}
}
class ThreadDemo{
public static void main(String args[]){
// Создание анонимного объекта класса NewThread:
new NewThread(); // Создание нового потока
try{
for(int i=5;i>0;i--){
System.out.println("Главный поток: "+i*100);
// Приостановка главного потока:
Thread.sleep(1000);}
}
// Обработка исключительной ситуации прерывания главного потока:
catch(InterruptedException e){
System.out.println("Прерывание главного потока!");}
System.out.println("Завершение главного потока!");}
}
В программе создается класс NewThread, реализующий интерфейс Runnable. Полем этого класса описана переменная t класса Thread. В конструкторе класса NewThread командой t=new Thread(this,"Новый поток") полю t в качестве значения присваивается ссылка на создаваемый объект класса Thread. Объект создается на основе объекта класса NewThread, полем которого является объектная переменная t (первый аргумент конструктора — ключевое слово this), а имя потока задается вторым аргументом конструктора — в данном случае это строка "Новый поток". Командой System.out.println("Дочерний поток: "+t) выводятся сведения о созданном потоке, а командой t.start() поток запускается. Еще раз отметим, что все эти действия описаны в конструкторе класса NewThread, то есть выполняются они при создании объекта этого класса.
В классе NewThread описан метод run(). Этим методом с интервалом задержки в 0,5 секунды выводится сообщение "Дочерний поток: " и натуральное число от 5 до 1 с шагом дискретности 1. Для приостановки потока использована команда Thread.sleep(500). Кроме того, в методе run() обрабатывается исключение InterruptedException (прерывание потока) для выполняемого потока.
В главном методе программы в классе ThreadDemo командой new NewThread() создается анонимный объект класса NewThread, чем автоматически запускается дочерний поток. После этого в рамках главного потока с интервалом задержки в одну секунду выводится сообщение "Главный поток: " и число от 500 до 100 с интервалом дискретности 100. Как и в случае дочернего потока, приостановка главного потока осуществляется вызовом метода sleep(), также отслеживается и обрабатывается исключение InterruptedException. В результате выполнения программы получаем:
Дочерний поток: Thread[Новый поток,5,main]
Главный поток: 500
Дочерний поток: 5
Дочерний поток: 4
Дочерний поток: 3
Главный поток: 400
Дочерний поток: 2
Дочерний поток: 1
Главный поток: 300
Завершение дочернего потока!
Главный поток: 200
Главный поток: 100
Завершение главного потока!
Фактически, при выполнении программы накладываются друг на друга два процесса (потока): главным потоком сообщения выводятся с интервалом одна секунда, а дочерним потоком сообщения выводятся с интервалом 0,5 секунды.
Поскольку количество выводимых в каждом из потоков сообщений одинаково, а интервал между сообщениями главного потока больше, чем интервал между сообщениями дочернего потока, первым заканчивается дочерний поток, а его сообщения появляются «кучнее».
Практически также создаются потоки наследованием класса Thread. Здесь уместно отметить, что класс Thread сам наследует интерфейс Runnable. Поэтому принцип создания потока остается неизменным, просто вместо непосредственной реализации в создаваемом классе интерфейса Runnable этот интерфейс реализуется опосредованно, путем расширения (наследования) класса Thread. Реализация метода run() в классе Thread не предполагает каких-либо действий. Выход из ситуации можно найти, расширив класс Thread путем создания подкласса. Как и в предыдущем случае с интерфейсом Runnable, в подклассе, создаваемом на основе класса Thread, необходимо описать (переопределить) метод run() и запустить его унаследованным из Thread методом start(). Правда, здесь есть одно отличие, которое, в принципе, упрощает ситуацию. Дело в том, что при создании объекта подкласса, расширяющего класс Thread, нет необходимости создавать объект класса Thread, как это было в предыдущем примере, когда в реализующем интерфейс Runnable классе определялось поле-объект класса Thread. Поток вызывается прямо из объекта подкласса. В листинге 3 приведен пример создания нового потока путем расширения класса Thread.
Листинг 3.Создание потока расширением класса Thread
// Класс NewThread расширяет класс Thread:
class NewThread extends Thread{
// Конструктор класса:
NewThread(){
// Вызов конструктора класса Thread:
super("Новый поток");
// Вывод сведений о потоке:
System.out.println("Дочерний поток: "+this);
// Запуск потока на выполнение:
start();
}
// Переопределение метода run():
public void run(){
try{
for(int i=5;i>0;i--){
System.out.println("Дочерний поток: "+i);
// Приостановка потока:
Thread.sleep(500);}
}
// Обработка исключения прерывания потока:
catch(InterruptedException e){
System.out.println("Прерывание дочернего потока!");}
System.out.println("Завершение дочернего потока!");}
}
class ExtendsThreadDemo{
public static void main(String args[]){
new NewThread();
try{
for(int i=5;i>0;i--){
System.out.println("Главный поток: "+i*100);
Thread.sleep(1000);}
}catch(InterruptedException e){
System.out.println("Прерывание главного потока!");}
System.out.println("Завершение главного потока!");
}}
В результате выполнения программы получаем следующее:
Дочерний поток: Thread[Новый поток,5,main]
Главный поток: 500
Дочерний поток: 5
Дочерний поток: 4
Дочерний поток: 3
Главный поток: 400
Дочерний поток: 2
Дочерний поток: 1
Главный поток: 300
Завершение дочернего потока!
Главный поток: 200
Главный поток: 100
Завершение главного потока!
Программа, фактически, такая же, как и в предыдущем случае. Однако создание потока реализовано по-иному. В классе NewThread, который наследует класс Thread, определяются конструктор и метод run(). В конструкторе командой super("Новый поток") вызывается конструктор класса Thread с аргументом — названием создаваемого потока. Вывод на экран информации о потоке осуществляется командой:
System.out.println("Дочерний поток: "+this)
Причем здесь в качестве ссылки на объект потока использована ссылка this на создаваемый объект. Запускается поток вызовом метода start() объекта. Во всем остальном программный код схож с кодом из рассмотренного ранее примера и, думается, особых комментариев не требует.
Дата добавления: 2016-06-22; просмотров: 2801;