Рассмотрим здесь же еще одно свойство потоков. Раньше, когда рассматривались однопоточные приложения, завершение вычислений однозначно приводило к завершению выполнения программы. Теперь же приложение должно работать до тех пор, пока есть хоть один действующий поток исполнения. В то же время часто бывают нужны обслуживающие потоки, которые не имеют никакого смысла, если они остаются в системе одни. Например, автоматический сборщик мусора в Java запускается в виде фонового (низкоприоритетного) процесса. Его задача – отслеживать объекты, которые уже не используются другими потоками, и затем уничтожать их, освобождая оперативную память. Понятно, что работа одного потока garbage collector 'а не имеет никакого смысла.
Такие обслуживающие потоки называют демонами ( daemon ), это свойство можно установить любому потоку. В итоге приложение выполняется до тех пор, пока есть хотя бы один поток не- демон.
Рассмотрим, как потоки реализованы в Java.
Базовые классы для работы с потоками
Класс Thread
Поток выполнения в Java представляется экземпляром класса Thread. Для того, чтобы написать свой поток исполнения, необходимо наследоваться от этого класса и переопределить метод run(). Например,
public class MyThread extends Thread { public void run() { // некоторое долгое действие, вычисление long sum=0; for (int i=0; i<1000; i++) { sum+=i; } System.out.println(sum); } }
Метод run() содержит действия, которые должны выполняться в новом потоке исполнения. Чтобы запустить его, необходимо создать экземпляр класса-наследника и вызвать унаследованный метод start(), который сообщает виртуальной машине, что требуется запустить новый поток исполнения и начать выполнять в нем метод run().
MyThread t = new MyThread(); t.start();
В результате чего на консоли появится результат:
499500
Когда метод run() завершен (в частности, встретилось выражение return ), поток выполнения останавливается. Однако ничто не препятствует записи бесконечного цикла в этом методе. В результате поток не прервет своего исполнения и будет остановлен только при завершении работы всего приложения.
Интерфейс Runnable
Описанный подход имеет один недостаток. Поскольку в Java множественное наследование отсутствует, требование наследоваться от Thread может привести к конфликту. Если еще раз посмотреть на приведенный выше пример, станет понятно, что наследование производилось только с целью переопределения метода run(). Поэтому предлагается более простой способ создать свой поток исполнения. Достаточно реализовать интерфейс Runnable, в котором объявлен только один метод – уже знакомый void run(). Запишем пример, приведенный выше, с помощью этого интерфейса:
public class MyRunnable implements Runnable { public void run() { // некоторое долгое действие, вычисление long sum=0; for (int i=0; i<1000; i++) { sum+=i; } System.out.println(sum); } }
Также незначительно меняется процедура запуска потока:
Runnable r = new MyRunnable(); Thread t = new Thread(r); t.start();
Если раньше объект, представляющий сам поток выполнения, и объект с методом run(), реализующим необходимую функциональность, были объединены в одном экземпляре класса MyThread, то теперь они разделены. Какой из двух подходов удобней, решается в каждом конкретном случае.
Подчеркнем, что Runnable не является полной заменой классу Thread, поскольку создание и запуск самого потока исполнения возможно только через метод Thread.start().
Работа с приоритетами
Рассмотрим, как в Java можно назначать потокам приоритеты. Для этого в классе Thread существуют методы getPriority() и setPriority(), а также объявлены три константы:
MIN_PRIORITY MAX_PRIORITY NORM_PRIORITY
Из названия понятно, что их значения описывают минимальное, максимальное и нормальное (по умолчанию) значения приоритета.
Рассмотрим следующий пример: