Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8332 / 610 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 9:

Дополнительные элементы объектного программирования на языке Java

< Лекция 8 || Лекция 9: 123 || Лекция 10 >

Класс Thread и интерфейс Runnable. Создание и запуск потока выполнения

Имеется два способа создать класс, экземплярами которого будут потоки выполнения: унаследовать класс от java.lang.Thread либо реализовать интерфейс java.lang.Runnable. Этот интерфейс имеет декларацию единственного метода public void run(), который обеспечивает последовательность действий при работе потока. При этом класс Thread уже реализует интерфейс Runnable, но с пустой реализацией метода run().Так что при создании экземпляра Thread создается поток, который ничего не делает. Поэтому в потомке надо переопределить метод run(). В нем следует написать реализацию алгоритмов, которые должны выполняться в данном потоке. Отметим, что после выполнения метода run() поток прекращает существование – "умирает".

Рассмотрим первый вариант, когда мы наследуем класс от класса Thread, переопределив метод run().

Объект-поток создается с помощью конструктора. Имеется несколько перегруженных вариантов конструкторов, самый простой из них – с пустым списком параметров. Например, в классе Thread их заголовки выглядят так:

  • public Thread() – конструктор по умолчанию. Подпроцесс получает имя "system".
  • public Thread(String name) - поток получает имя, содержащееся в строке name.

Также имеется возможность создавать потоки в группах. Но в связи с тем, что данная технология устарела и не нашла широкого распространения, о группах потоков выполнения в данном учебном пособии рассказываться не будет.

В классе-потомке можно вызывать конструктор по умолчанию (без параметров), либо задать свои конструкторы, используя вызовы прародительских с помощью вызова super ( список параметров ). Из-за отсутствия наследования конструкторов в Java приходится в наследнике заново задавать конструкторы с той же сигнатурой, что и в классе Thread. Это является простой, но утомительной работой. Именно поэтому обычно предпочитают способ задания класса с реализацией интерфейса Runnable, о чем будет рассказано несколькими строками позже.

Создание и запуск потока осуществляется следующим образом:

public class T1 extends Thread{
   public void run(){
     ...
   }
   ...
}

Thread thread1= new T1();
thread1.start();

Второй вариант – использование класса, в котором реализован интерфейс java.lang.Runnable. Этот интерфейс, как уже говорилось, имеет единственный метод public void run(). Реализовав его в классе, можно создать поток с помощью перегруженного варианта конструктора Thread:

public class R1 implements Runnable{
   public void run(){
     ...
   }
   ...
}

Thread thread1= Thread( new R1() );
thread1.start();

Обычно таким способом пользуются гораздо чаще, так как в разрабатываемом классе не приходится заниматься дублированием конструкторов класса Thread. Кроме того, этот способ можно применять в случае, когда уже имеется класс, принадлежащий иерархии, в которой базовым классом не является Thread или его наследник, и мы хотим использовать этот класс для работы внутри потока. В результате от этого класса мы получаем метод run(), в котором реализован нужный алгоритм, и этот метод работает внутри потока типа Thread, обеспечивающего необходимое поведение в многопоточной среде. Однако в данном случае затрудняется доступ к методам из класса Thread – требуется приведение типа.

Например, чтобы вывести информацию о приоритете потока, в первом способе создания потока в методе run() надо написать оператор

System.out.println("Приоритет потока="+this.getPriority());

А во втором способе приходится это делать в несколько этапов. Во-первых, при задании класса нам следует добавить в объекты типа R1 поле thread:

public class R1 implements Runnable{
    public Thread thread;
    
    public void run() {
        System.out.println("Приоритет потока="+thread.getPriority());
    }
}

С помощью этого поля мы будем добираться до объекта-потока. Но теперь после создания потока необходимо не забыть установить для этого поля ссылку на созданный объект-поток. Так что создание и запуск потока будет выглядеть так:

R1 r1=new R1();
Thread thread1=new Thread(r1, "thread1");
r1.thread=thread1;
thread1.start();//либо, что то же, r1.thread.start()

Через поле thread мы можем получать доступ к потоку и всем его полям и методам в алгоритме, написанном в методе run(). Указанные выше дополнительные действия – это всего три лишних строчки программы (первая - R1 r1=new R1(); вторая - r1.thread=thread1; третья - объявление в классе R1 - public Thread thread; ).

Как уже говорилось ранее, напрямую давать доступ к полю данных – дурной тон программирования. Исправить этот недостаток нашей программы просто: в дереве элементов программы окна Projects в разделе Fields ("поля") щелкнем правой кнопкой мыши по имени thread и выберем в появившемся всплывающем меню Refactor/Encapsulate Fields… ("Провести рефакторинг"/ "Инкапсулировать поля…"). В появившемся диалоге нажмем на кнопку "Next>" и проведем рефакторинг, подтвердив выбор в нижнем окне.

В классе Thread имеется несколько перегруженных вариантов конструктора с параметром типа Runnable:

  • public Thread(Runnable target) – с именем "system" по умолчанию.
  • public Thread(Runnable target, String name) – с заданием имени.

Также имеются варианты с заданием группы потоков.

Поля и методы, заданные в классе Thread

В классе Thread имеется ряд полей данных и методов, про которые надо знать для работы с потоками.

Важнейшие константы и методы класса Thread:
  • MIN_PRIORITY – минимально возможный приоритет потоков. Зависит от операционной системы и версии JVM. На компьютере автора оказался равен 1.
  • NORM_PRIORITY - нормальный приоритет потоков. Главный поток создается с нормальным приоритетом, а затем приоритет может быть изменен. На компьютере автора оказался равен 5.
  • MAX_PRIORITY – максимально возможный приоритет потоков. На компьютере автора оказался равен 10.
  • static int activeCount() - возвращает число активных потоков приложения.
  • static Thread currentThread() – возвращает ссылку на текущий поток.
  • boolean holdsLock(Object obj) – возвращает true в случае, когда какой-либо поток (то есть текущий поток) блокирует объект obj.
  • static boolean interrupted() – возвращает состояние статуса прерывания текущего потока, после чего устанавливает его в значение false.
Важнейшие методы объектов типа Thread:

  • void run() – метод, который обеспечивает последовательность действий во время жизни потока. В классе Thread задана его пустая реализация, поэтому в классе потока он должен быть переопределен. После выполнения метода run() поток умирает.
  • void start() – вызывает выполнение текущего потока, в том числе запуск его метода run() в нужном контексте. Может быть вызван всего один раз.
  • void setDaemon(boolean on) – в случае on==true устанавливает потоку статус демона, иначе – статус пользовательского потока.
  • boolean isDaemon() - возвращает true в случае, когда текущий поток является демоном.
  • void yield() – "поступиться правами" – вызывает временную приостановку потока, с передачей права другим потокам выполнить необходимые им действия.
  • long getId() – возвращает уникальный идентификатор потока. Уникальность относится только ко времени жизни потока - после его завершения (смерти) данный идентификатор может быть присвоен другому создаваемому потоку.
  • String getName() – возвращает имя потока, которое ему было задано при создании или методом setName.
  • void setName(String name) – устанавливает новое имя потока.
  • int getPriority() - возвращает приоритет потока.
  • void setPriority(int newPriority) – устанавливает приоритет потока.
  • void checkAccess() – осуществление проверки из текущего потока на позволительность доступа к другому потоку. Если поток, из которого идет вызов, имеет право на доступ, метод не делает ничего. Иначе – возбуждает исключение SecurityException.
  • String toString() – возвращает строковое представление объекта потока, в том числе – его имя, группу, приоритет.
  • void sleep(long millis) – вызывает приостановку ("засыпание") потока на millis миллисекунд. При этом все блокировки (мониторы) потока сохраняются. Перегруженный вариант sleep(long millis,int nanos) - параметр nanos задает число наносекунд. Досрочное пробуждение осуществляется методом interrupt() – с возбуждением исключения InterruptedException.
  • void interrupt() – прерывает "сон" потока, вызванный вызовами wait(…) или sleep(…), устанавливая ему статус прерванного (статус прерывания=true). При этом возбуждается проверяемая исключительная ситуация InterruptedException.
  • boolean isInterrupted() - возвращает текущее состояние статуса прерывания потока без изменения значения статуса.
  • void join() – "слияние". Переводит поток в режим умирания - ожидания завершения (смерти). Это ожидание – выполнение метода join() - может длиться сколь угодно долго, если соответствующий поток на момент вызова метода join() блокирован. То есть если в нем выполняется синхронизованный метод или он ожидает завершения синхронизованного метода. Перегруженный вариант join(long millis) - ожидать завершения потока в течение millis миллисекунд. Вызов join(0) эквивалентен вызову join(). Еще один перегруженный вариант join(long millis,int nanos) - параметр nanos задает число наносекунд. Ожидание смерти может быть прервано другим потоком с помощью метода interrupt() – с возбуждением исключения InterruptedException. Метод join() является аналогом функции join в UNIX. Обычно используется для завершения главным потоком работы всех дочерних пользовательских потоков ("слияния" их с главным потоком).
  • boolean isAlive() - возвращает true в случае, когда текущий поток жив (не умер). Отметим, что даже если поток завершился, от него остается объект-"призрак", отвечающий на запрос isAlive() значением false – то есть сообщающий, что объект умер.

Следует отметить, что все ведущие разработчики процессоров перешли на многоядерную технологию. При этом в одном корпусе процессора расположено несколько процессорных ядер, способных независимо выполнять вычисления, но они имеют доступ к одной и той же общей памяти. В связи с этим программирование в многопоточной среде признано наиболее перспективной моделью параллелизации программ и становится одним из важнейших направлений развития программных технологий. Модель многопоточности Java позволяет весьма элегантно реализовать преимущества многоядерных процессорных систем. Во многих случаях программы Java, написанные с использованием многопоточности, эффективно распараллеливаются автоматически на уровне виртуальной машины - без изменения не только исходного, но даже скомпилированного байт-кода приложений.

Но программирование в многопоточной среде является сложным и ответственным занятием, требующим очень высокой квалификации. Многие алгоритмы, кажущиеся простыми, естественными и надежными, в многопоточной среде оказываются неработоспособными. Из-за чего способы решения даже самых простых задач становятся необычными и запутанными, не говоря уж о проблемах, возникающих при решении сложных задач. В связи с этим автор рекомендует на начальном этапе не увлекаться многопоточностью, а только ознакомиться с данной технологией.

Тем, кто все же хочет заняться таким программированием, рекомендуется сначала прочитать главу 9 в книге Джошуа Блоха [8].

< Лекция 8 || Лекция 9: 123 || Лекция 10 >
Максим Старостин
Максим Старостин

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.