Поля и методы, заданные в классе 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].

Подключение внешних библиотек DLL.“Родные” (native) методы*

*- данный параграф приводится в ознакомительных целях

Для прикладного программирования средств Java в подавляющем большинстве случаев хватает. Однако иногда возникает необходимость подключить к программе ряд системных вызовов. Либо обеспечить доступ к библиотекам, написанным на других языках программирования. Для таких целей в Java используются методы, объявленные с модификатором native –“родной”. Это слово означает, что при выполнении метода производится вызов “родного” для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок “родного” метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки (DLL – Dynamic Link Library под Windows, Shared Objects под UNIX-образными операционными системами).

Правило для объявления и реализации таких методов носит название JNI – Java Native Interface.

Объявление “родного” метода в Java имеет вид

Модификаторы native ВозвращаемыйТип имяМетода(список параметров);

Тело “родного” метода не задаётся – оно является внешним и загружается в память компьютера с помощью загрузки той библиотеки, из которой этот метод должен вызываться:

System.loadLibrary(“ИмяБиблиотеки”);

При этом имя библиотеки задаётся без пути и без расширения. Например, если под Windows библиотека имеет имя myLib.dll, или под UNIX или Linux имеет имя myLib.so , надо указывать System.loadLibrary(“myLib”);

В случае, если файла не найдено, возбуждается непроверяемая исключительная ситуация UnsatisfiedLinkError.

Если требуется указать имя библиотеки с путём, применяется вызов

System.load (“ИмяБиблиотекиСПутём”);

Который во всём остальном абсолютно аналогичен вызову loadLibrary.

После того, как библиотека загружена, с точки зрения использования в программе вызов “родного” метода ничем не отличается от вызова любого другого метода.

 

Для создания библиотеки с методами, предназначенными для работы в качестве “родных”, обычно используется язык С++. В JDK существует утилита javah.exe, предназначенная для создания заголовков C++ из скомпилированных классов Java. Покажем, как ей пользоваться, на примере класса ClassWithNativeMethod. Зададим его

в пакете нашего приложения:

 

package java_example_pkg;

public class ClassWithNativeMethod {

 

/** Creates a new instance of ClassWithNativeMethod */

public ClassWithNativeMethod() {

}

 

public native void myNativeMethod();

}

 

Для того, чтобы воспользоваться утилитой javah, скомпилируем проект и перейдём в папку build\classes . В ней будут располагаться папка с пакетом нашего приложения java_example_pkg и папка META-INF. В режиме командной строки выполним команду

 

javah.exe java_example_pkg.ClassWithNativeMethod

 

- задавать имя класса необходимо с полной квалификацией, то есть с указанием перед ним имени пакета. В результате в папке появится файл java_example_pkg_ClassWithNativeMethod.h со следующим содержимым:

 

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class java_example_pkg_ClassWithNativeMethod */

 

#ifndef _Included_java_example_pkg_ClassWithNativeMethod

#define _Included_java_example_pkg_ClassWithNativeMethod

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class: java_example_pkg_ClassWithNativeMethod

* Method: myNativeMethod

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod

(JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif

 

Функция Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod(JNIEnv *, jobject), написанная на C++, должна обеспечивать реализацию метода myNativeMethod() в классе Java. Имя функции C++ состоит из: префикса Java, разделителя “_”, модифицированного имени пакета (знаки подчёркивания “_” заменяются на “_1”), разделителя “_”, имени класса, разделителя “_”, имени “родного” метода.

Первый параметр JNIEnv * в функции C++ обеспечивает доступ “родного” кода к параметрам и объектам, передающимся из функции C++ в Java. В частности, для доступа к стеку. Второй параметр, jobject , – ссылка на экземпляр класса, в котором задан “родной” метод, для методов объекта, и jclass – ссылка на сам класс – для методов класса. В языке C++ нет ссылок, но в Java все переменные объектного типа являются ссылками. Соответственно, второй параметр отождествляется с этой переменной.

Если в “родном” методе имеются параметры, то список параметров функции C++ расширяется. Например, если мы зададим метод

public native int myNativeMethod(int i);

то список параметров функции C++ станет

(JNIEnv *, jobject, jint)

А тип функции станет jint вместо void.

Соответствие типов Java и C++:

 

Тип Java Тип JNI (C++) Характеристика типа JNI
boolean jboolean 1 байт, беззнаковый
byte jbyte 1 байт
char jchar 2 байта, беззнаковый
short jshort 2 байта
int jint 4 байта
long jlong 8 байт
float jfloat 4 байта
double jdouble 8 байт
void void -
Object jobject Базовый для остальных классов
Class jclass Ссылка на класс Java
String jstring Строки Java
массив jarray Базовый для классов массивов
Object[] jobjectArray Массив объектов
boolean[] jbooleanArray Массив булевских значений
byte[] jbyteArray Массив байт (знаковых значений длиной в байт)
char[] jcharArray Массив кодов символов
short[] jshortArray Массив коротких целых
int[] jintArray Массив целых
long[] jlongArray Массив длинных целых
float[] jfloatArray Массив значений float
double[] jdoubleArray Массив значений double
Throwable jthrowable Обработчик исключительных ситуаций

 

В реализации метода требуется объявить переменные. Например, если мы будем вычислять квадрат переданного в метод значения и возвращать в качестве результата значение параметра, возведённое в квадрат (пример чисто учебный), код реализации функции на C++ будет выглядеть так:

 

#include ”java_example_pkg_ClassWithNativeMethod.h”

JNIEXPORT jint JNICALL Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod

(JNIEnv *env, jobject obj, jint i ){

return i*i

};

 

Отметим, что при работе со строками и массивами для получения и передачи параметров требуется использовать переменную env. Например, получение длины целого массива, переданного в переменную jintArray intArr, будет выглядеть так:

jsize length=(*env)->GetArrayLength(env, intArr);

Выделение памяти под переданный массив:

jint *intArrRef=(*env)->GetIntArrayElements(env, intArr,0);

Далее с массивом intArr можно работать как с обычным массивом C++.

Высвобождение памяти из-под массива:

(*env)->ReleaseIntArrayElements(env, intArr, intArrRef ,0);

 

Имеются аналогичные функции для доступа к элементам массивов всех примитивных типов:

GetBooleanArrayElements, GetByteArrayElements,…, GetDoubleArrayElements. Эти функции копируют содержимое массивов Java в новую область памяти, с которой и идёт работа в C++. Для массивов объектов имеется не только функция GetObjectArrayElement, но и SetObjectArrayElement – для получения и изменения отдельных элементов таких массивов.

Строка Java jstring s преобразуется в массив символов C++ так:

const char *sRef=(*env)->GetStringUTFChars(env,s,0);

Её длина находится как int s_len=strlen(sRef);

Высвобождается из памяти как

(*env)->ReleaseStringUTFChars(env,s,sRef);

 

Краткие итоги по главе 9

ü Программу, выполняющуюся под управлением операционной системы, называют процессом (process), или, что то же, приложением. У каждого процесса своё адресное пространство. Потоки выполнения (threads) отличаются от процессов тем, что выполняются в адресном пространстве своего родительского процесса. Потоки выполняются параллельно (псевдопараллельно), но, в отличие от процессов, легко могут обмениваться данными в пределах общего виртуального адресного пространства. То есть у них могут иметься общие переменные, в том числе – массивы и объекты.

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

ü Любая программа Java неявно использует потоки. В главном потоке виртуальная Java-машина (JVM) запускает метод main приложения, а также все методы, вызываемые из него. Главному потоку автоматически даётся имя ”main”.

ü Если разные потоки получают доступ к одним и тем же данным, причём один из них или они оба меняют эти данные, для них требуется обеспечить установить разграничение доступа. Пока один поток меняет данные, второй не должен иметь права их читать или менять. Он должен дожидаться окончания доступа к данным первого потока. Говорят, что осуществляется синхронизация потоков. В Java для этих целей служит оператор synchronize (“синхронизировать”). Иногда синхронизованную область кода (метод или оператор) называют критической секцией кода.

ü При запуске синхронизованного метода говорят, что объект входит в монитор, при завершении – что объект выходит из монитора. При этом поток, внутри которого вызван синхронизованный метод, считается владельцем данного монитора.

ü Имеется два способа синхронизации по ресурсам: синхронизация объекта и синхронизация метода.

Синхронизация объекта obj1 при вызове несинхронизованного метода: synchronized(obj1) оператор;

Синхронизация метода с помощью модификатора synchronized при задании класса:

public synchronized тип метод(...){...}

ü Кроме синхронизации по данным имеется синхронизация по событиям, когда параллельно выполняющиеся потоки приостанавливаются вплоть до наступления некоторого события, о котором им сигнализирует другой поток. Основными операциями при таком типе синхронизации являются wait (“ждать”) и notify (“оповестить”).

ü Имеется два способа создать класс, экземплярами которого будут потоки: унаследовать класс от java.lang.Thread либо реализовать интерфейс java.lang.Runnable.

ü Интерфейс java.lang.Runnable имеет декларацию единственного метода public void run(), который обеспечивает последовательность действий при работе потока. Класс Thread уже реализует интерфейс Runnable, но с пустой реализацией метода run(). Поэтому в потомке Thread надо переопределить метод run().

ü При работе с большим количеством потоков требуется их объединение в группы. Такая возможность инкапсулируется классом TreadGroup (“Группа потоков”).

ü Для получения доступа к библиотекам, написанным на других языках программирования, в Java используются методы, объявленные с модификатором native –“родной”. При выполнении такого метода производится вызов “родного” для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок “родного” метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки.

 

Задания

  • Написать приложение, в котором используются потоки. Использовать задание класса потока как наследника Thread. Класс потока должен обеспечивать в методе run построчный несинхронизированный вывод в консольное окно чисел от 1 до 100 порциями по 10 чисел в строке, разделённых пробелами, причём перед каждой такой порцией должна стоять надпись “Thread 1:” для первого потока, “Thread 2:” для второго, и т.д. Для вывода строки задать в классе метод print10. В приложении по нажатии на первую кнопку должны создаваться два или более потоков, а при нажатии на вторую они должны стартовать.
  • Усовершенствовать приложение, обеспечив синхронизацию за счёт объявления вызова print10 в методе run синхронизированным.
  • Создать копию класса потока, отличающуюся от первоначальной тем, что выводятся числа от 101 до 200, класс задан как реализующий интерфейс Runnable, а метод print10 задан как синхронизированный. Добавить в приложение создание и старт потоков – экземпляров данного класса.

 


Глава 10. Введение в сетевое программирование