Использование языка Java для разработки параллельных приложений
Б.И.Илюшкин
Особенностью языка Java в отличие от других языков(C,C++,Fortran)
является наличие встроенной поддержки многопоточного программирования.
Поток выполнения (thread) - элемент кода программы,выполняемый последовательно.Иногда
потоки выполнения называют подпроцессами или нитями процесса.В многопоточных
программах потоки выполняются асинхронно,совместно используя сегменты данных
и кода процесса.Организация взаимодействия между потоками гораздо проще
чем между процессами с использованием внешних функций API.Каждый порожденный
процесс имеет собственную копию виртуального адресного пространства и использует
больше системных ресурсов.Процессами управляет ядро ОС.
Многопоточность Java позволяет осуществить многозадачность внутри
одной программы.В Java используется достаточно простая модель синхронизации
потоков.Организация связи и контекстное переключение(context switch)
потоков контролируется исполняющей системой Java.Унифицированный
многопоточный интерфейс поддерживается всеми виртуальными машинами Java.
Многопоточность реализуется достаточно просто при разработке объектно
ориентированного приложения Java.Такое приложение состоит из набора
объектов, взаимодействующих друг с другом.Каждый объект - независимый компонент,который
может выполняться потоком и запускаться параллельно с другими объектами.
Создание потоков.
Когда Java приложение запускается на выполнение,то автоматически
создается один поток,который называется главным(main thread),так
как зто единственный поток,выполняющийся при запуске программы.Главный
поток вызывает метод main(),обеспечивающий основную логику его
выполнения.Метод
main() является статическим,поскольку программа
запускается только с одним
main(). Для запуска новых потоков используется
метод run(),который реализует основную логику выполнения потока
и объявляется как
public void run()
Обеспечить реализацию метода run() можно двумя способами:путем
расширения класса Thread и через интерфейс Runnable.
Расширение класса Thread
Класс Thread скрывает механизм запуска и контроля выполнения параллельного
потока.Необходимо создать новый класс,который расширяет класс
Thread
,а затем создать экземпляр этого класса.В расширенном классе нужно заменить
метод run() класса Thread ,который является точкой входа
для нового потока.Для запуска потока нужно вызвать метод start().
Реализация интерфейса Runnable
Интерфейс позволяет определить,что должен делать класс.Объявленные в нем
методы не имеют тела.Назначение интерфейса - обеспечить динамический выбор
метода по ходу выполнения программы.
Необходимо описать класс,который реализует интерфейс Runnable
(выполняемый),а затем создать экземпляр данного класса.Интерфейс
Runnable
определяет только один абстрактный метод с именем run(),являющийся
точкой входа потока.Позтому достаточно реализовать метод run(),внутри
которого поместить операторы,которые должны выполняться в новом потоке.Далее
необходимо создать экземпляр класса Thread,передав ему объект
Runnable.Для
поддержки интерфейса Runnable в ряд конструкторов класса
Thread
был введен отдельный параметр Runnable.Для запуска потока нужно
вызвать метод start(). При выполнении поток будет вызывать метод
run() объекта
Runnable.
Структура программы,запускающей два потока,созданных обоими способами,
выглядит следующим образом
//Создание 1-го потока путем расширения класса Thread
class One extends Thread {
// точка входа 1-го потока
public void run() {
. . . . . . . . . . . . . . . . .
выполнение 1-го потока
. . . . . . . . . . . . . . . . .
}
}
//Создание 2-го потока путем реализации интерфейса Runnable
class Two implements Runnable {
// точка входа 2-го потока
public void run() {
. . . . . . . . . . . . . . . . .
выполнение 2-го потока
. . . . . . . . . . . . . . . . .
}
}
// запуск программы
public class OneTwo {
public static void main(String args[]) {
// создание экземпляров классов
One c = new One() ;
Runnable r = new Two() ;
Thread t = new Thread(r) ; // передача объекта Runnable классу Thread
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
// запуск потоков
c.start() ;
t.start() ;
}
}
Состояния потоков
Поток Java может находиться в одном из следующих состояний в течение
периода существования :
-
новый (new thread)
-
исполняемый (runnable or ready to run)
-
неисполняемый (not runnable)
-
пассивный (dead)
Новый - поток создан,но еще не запущен.
Исполняемый - поток запущен и готов продолжить выполнение,т.е.ему
может быть выделено операционной системой процессорное время(когда процессор
окажется свободным).Поток,которому выделено процессорное время,является
выполняющимся(running).
Неисполняемый - в данное состояние поток переходит после
наступления определенного события(ожидание завершения операции ввода/вывода,перевод
в неактивный режим на определенное время методом sleep(),вызов
методов
wait() или suspend()).Неисполняемый поток становится
опять исполняемым при изменении его состояния (завершен ввод/вывод,завершен
период пребывания в неактивном режиме и т.д.).В течение периода своего
существования поток может неоднократно переходить из состояния
исполняемый
в состояние неисполняемый.
Пассивный - поток становится пассивным,когда он завершается.Обычно
поток становится пассивным,когда завершается его метод run().Кроме
того,поток может стать пассивным при вызове его методов stop()
или
destroy().
Пассивный поток является таковым постоянно,воскресить
его невозможно.
Диспетчеризация потоков
Диспетчеризация или планирование(scheduling) потоков представляет
собой механизм,используемый для определения способа выделения времени ЦП
для исполняемых потоков.Для виртуальной машины Java исполняемый
поток с наивысшим приоритетом всегда выбирается для выполнения.Механизм
реализации потоков Java является вытесняющим(preemptive),т.е.
диспетчер приостанавливает поток,если появится поток с более высоким приоритетом.
Как выполняются потоки с одинаковым приоритетом,в спецификации Java
не определено.Порядок их выполнения определяется операционной системой.
В реализации Java на Windows 95/NT используется базовый
диспетчер потоков с выделением квантов времени(выделяется промежуток
времени для каждого потока).В реализации Java на Solaris
используется диспетчер потоков без выделения квантов времени.Позтому
при реализации Java приложений на конкретной платформе необходимо
учитывать метод организации многозадачности в операционной системе.В случае
диспетчера с выделением квантов времени, контекстное переключение(context
switch) будет выполняться даже при отсутствии добровольной передачи
управления(yield()) или блокировки ввода/ вывода.Соответственно,поток
с более высоким приоритетом использует больше процессорного времени.В случае
диспетчера без выделения квантов времени контекстного переключения потоков
не будет.Поток с наибольшим приоритетом использует 100% времени ЦП(в случае
равных приоритетов потоки будут выполняться последовательно,один за другим).В
данном случае для контекстного переключения можно использовать методы перевода
потоков в неактивный режим на определенный период времени sleep()
или метод явной передачи управления yield().
Пример Простая 2-х поточная программа с переключением потоков
class One extends Thread {
public void run() {
for (int i=0; i<8; i++) {
System.out.println("One");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
}
class Two implements Runnable {
public void run() {
for (int i=0; i<8; i++) {
System.out.println("Two");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
}
public class OneTwo {
public static void main(String args[]) {
One c = new One();
Runnable r = new Two();
Thread t = new Thread(r);
Thread m = Thread.currentThread();
System.out.println("Main thread : " + m);
System.out.println("First thread : " + c);
System.out.println("Second thread : " + t);
c.start();
t.start();
}
}
Листинг результата выполнения
Main thread : Thread[main,5,main]
First thread : Thread[Thread-2,5,main]
Second thread : Thread[Thread-3,5,main]
One
Two
One
Two
One
Two
One
Two
One
Two
One
Two
One
Two
One
Two
Метод sleep() используется для контекстного
переключения потоков. Без него,например, на платформе Solaris переключения
потоков не будет.
Интерфейс класса Thread
Класс Thread предназначен для создания нового потока.Он определяет
следующие основные конструкторы :
Thread()
Thread(Runnable object)
Thread(Runnable object,String name)
Thread(String name)
где
name - имя,присваиваемое потоку.
object - экземпляр объекта Runnable
. Если имя не присвоено,система сгенерирует уникальное имя в виде Thread-N,
где N - целое число.
Имеется еще три конструктора,предназначенные для присоединения потоков
к группам потоков,созданных конструкторами класса Thread Group.Данный
класс предназначен для создания группы потоков.
Некоторые из методов управления потоками
void start() - начинает выполнение потока
final void stop() - заканчивает выполнение потока
static void sleep(long msec) - прекращает выполнение потока на указанное
количество миллисекунд
static void yield() - заставляет поток передать ресурсы процессора
другому потоку
final void suspend() - приостанавливает выполнение потока
final void resume() - возобновляет выполнение потока
Синхронизация потоков
Для управления параллельным выполнением потоков,в частности для обеспечения
доступа к совместно используемым ресурсам,используется синхронизация.Механизм
синхронизации основывается на концепции монитора.
Монитор - это объект специального назначения,в котором применен
принцип взаимного исключения(mutual exclusion) для групп процедур.Во
время выполнения программы монитор допускает лишь поочередное выполнение
процедуры,находящейся под его контролем.
У каждого объекта в Java имеется свой собственный неявный монитор.Когда
метод типа synchronized вызывается для объекта,происходит обращение
к монитору объекта чтобы определить,выполняет ли в данный момент какой-либо
другой поток метод типа synchronized для данного объекта.Если
нет,то текущий поток получает разрешение войти в монитор.Вход в монитор
называется также блокировкой(locking) монитора.Если при этом
другой поток уже вошел в монитор,то текущий поток должен ожидать до тех
пор,пока другой поток не покинет монитор.Таким образом монитор Java
вводит поочередность в параллельную обработку.Этот способ называется также
преобразованием в последовательную форму(serialization).
Объявление метода synchronized не подразумевает,что только
один поток может одновременно выполнять этот метод,как в случае критического
участка (critical sections).Имеется ввиду,что в любой момент времени
только один поток может вызвать этот метод(или любой другой метод типа
synchronized)
для конкретного объекта.Таким образом,мониторы Java связаны с объектами,
но не с блоками кода.Два потока могут параллельно выполнять один и тот
же метод типа synchronized при условии,что этот метод вызван для
разных объектов.
Мониторы не являются объектами языка Java,у них нет атрибутов
или методов. Доступ к мониторам возможен на уровне собственного кода JVM.
В Java есть два способа синхронизации потоков.
1. Создание синхронизирующего метода внутри класса.
Используется при наличии метода или группы методов,обрабатывающих внутреннее
состояние объекта в многопоточной ситуации.Для организации последовательного
доступа потоков к методу объявление метода предваряется ключевым словом
synchronized.
class Callme{
synchronized void call(String msg){
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
}
2. Создание синхронизирующего блока.
Используется для организации доступа к объектам класса,не разработанного
для многопоточного доступа(например к массивам) или созданного другим программистом(нет
доступа к исходному коду).Необходимо поместить вызовы методов в синхронизирующий
блок путем использования оператора
synchronized, имеющего следующий
синтаксис :
synchronized (object){
// операторы,которые необходимо синхронизировать
}
Оператор synchronized полезен при непосредственном изменении общих
переменных объекта.
Пример
void call(SomeClassobj){
synchronized(obj){
obj.variable=5;
}
}
Следует учесть,что синхронизированные методы выполняются медленнее их несинхронизированных
аналогов.
Ниже приводится классический тестовый пример вычисления Пи на
Java
******
* Pi.java
*
* compute pi by integrating f(x) = 4/(1 + x**2)
*
* each thread:
* - receives the interval used in the approximation
* - calculates the areas of it's rectangles
* - synchronizes for a global summation
* process 0 prints the result and the time it took
******/
class Pi extends Thread
{
int from,to ;
static int nn,n =72000000,np = 4 ; // np - number of processors
double h,sum,x;
static double ssum = 0.0 ;
static int j ;
static long st,end ;
public Pi(int from, int to)
{
this.from = from;
this.to = to;
}
public double f(double a)
{
return(4.0 / (1.0 + a*a));
}
// synchronization
synchronized void count()
{
j = j + 1 ;
ssum += h * sum ;
System.out.println("ssum == " + ssum);
if ( j == np )
System.out.println(" ssum - pi = " + (ssum - Math.PI));
}
public void run()
{
st = System.currentTimeMillis(); // start
System.out.println(this);
h = 1.0 / (double) n;
sum = 0.0;
for (int i=from; i<to; i++)
{
// System.out.println("i== " + i);
x = h * ((double) i - 0.5);
sum += f(x);
}
count() ;
end = System.currentTimeMillis(); // end
System.out.println("Time == " + (end - st));
}
public static void main(String[] args)
{
j = 0 ;
// spawn np threads, each of wich calculates one area
for (int i=0; i<np; i++)
{
Pi t = new Pi(i*n/np, (i+1)*n/np);
t.start();
}
// System.out.println(" ssum - pi = " + (ssum - Math.PI));
}
}
Листинг результата для Solaris 2.5.1 UltraSparc 450 Server
(JDK 1.1.7)
Строка запуска : /bin/time java Pi
Thread[Thread-2,5,main]
ssum == 0.9799146557752786
Time == 8284
Thread[Thread-3,5,main]
ssum == 1.8545904471141887
Time == 8224
Thread[Thread-4,5,main]
ssum == 2.574004455173001
Time == 8306
Thread[Thread-5,5,main]
ssum == 3.1415926813673596
ssum - pi = 2.777756646921148E-8
Time == 8243
real 33.7
user 33.3
sys 0.1
Листинг результата для Solaris 2.6 UltraSparc 450 Server (JDK
1.2).
Thread[Thread-1,5,main]
Thread[Thread-4,5,main]
Thread[Thread-2,5,main]
Thread[Thread-3,5,main]
ssum == 1.8545904471141887
ssum == 2.574004455173001
ssum == 0.9799146557752786
Time == 4106
Time == 4107
Time == 4107
ssum == 3.1415926813673596
ssum - pi = 2.777756646921148E-8
Time == 4127
real 4.6
user 16.6
sys 0.2
Новая версия JDK 1.2 для Solaris 2.6
поддерживает многопроцессорную многопоточность (в данном случае 4-х процессорную)
и более производительный
JIT компилятор.Поэтому существенный выигрыш
в скорости.При этом реальное время счета(Real time) Java-кода
примерно только на 25% больше по сравнению с результатом,полученным для
Fortran-кода с использованием лицензионного оптимизированного Fortran
WorkShop компилятора.
Fortran-код вычисления Пи.
program main
c
real tt1(2),tt2(2)
integer*4 i,n
double precision sum,h,f,x
f(x)=4.d0/(1.d0+x*x)
n=72000000
c t1=dtime(tt1) !first call
t1=etime(tt2)
h=1.d0/n ! step
sum=0.0d0
c
do 10 i=1,n
sum=sum+h*f((i-.50)*h)
10 continue
c
t2=etime(tt2) ! second call
print *,'Real time =',t2
print *,' Sum =',sum
print *,'Sum - pi =',sum - 3.14159265358979323846d0
stop
end
Имеется Java version Linpack Bencmark test для измерения производительности
операций с плавающей точкой процессора.
http://www.csa.ru/CSA/tutor/Linpack.java"
Согласование потоков.
Синхронизация позволяет осуществлять блокировку потоков,предотвращая асинхронный
доступ к определенным методам.Однако часто возникает необходимость согласования
потоков,когда выполнение одного потока может зависеть от завершения в другом
потоке запроса на обслуживание или выполнения определенной операции.При
этом важно,чтобы ожидающий поток или потоки ожидали,не используя время
ЦП на опрос для постоянной проверки некоторых условий.Чтобы избежать потери
времени,связанные с опросом,Java использует элегантный механизм
взаимодействия между потоками через методы
wait(),notify(),notifyall().Все
три метода объявлены в классе
Object.
final void wait()
final void notify()
final void notifyall()
wait() - предписывает вызвавшему потоку отдать монитор и перейти
в состояние ожидания,пока какой-нибудь другой поток не войдет в тот же
монитор и не вызовет метод notify().
notify() - активизирует один из ожидающих потоков,вызвавших
метод wait() того же объекта.Запускается поток с наибольшим приоритетом.
notifyall() - активизирует все ожидающие потоки,вызвавшие метод
wait() того же объекта.
Метод wait() имеет дополнительную форму,позволяющую задать
период ожидания. Все три метода служат интерфейсом для взаимодействия с
монитором объекта и их можно вызвать только в том случае,когда текущий
поток владеет правами на монитор объекта,т.е. внутри метода или блока типа
sysnchronized.
Мониторы используются как для введения поочередности в параллельную
обработку,так и для согласования потоков.Вызов метода wait() для
данного объекта приостанавливает текущий поток и вводит его в очередь ожидания
по условию (condition variable wait queue) в мониторе объекта.Очередь
содержит список всех потоков,заблокированных внутри метода wait()для
данного объекта.Вызов метода notify() переводит единственный поток
в активный режим,уведомляя о том,что условие изменилось.
Обычно wait() помещается в блок try{} обработки исключительных
ситуаций.
synchronized int get() {
while( условие )
{ try {
wait();} catch() {}
}
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
notify();
. . . . . . . . . . . . . . . .
}
Вызов wait() и notify() для массивов осуществляется внутри синхронизирующего
блока.
Obj[]array = getArray();
synchronized(array){
array.wait();
}
. . . . . . . . . . . . .
. . . . . . . . . . . . .
synchronized(array){
array.notify();
}
Среда выполнения Java
Компилятор языка Java преобразует исходный код Java в платформо-
независимый байткод.Байткод - это оптимизированный набор команд, предназначенный
для выполнения в программном окружении,называемым Исполняющая система
Java(Java Runtime Envinronment - JRE).
JRE реализуется для каждой операционной системы и содержит библиотеки
классов Java и Java Virtual Machine(JVM).
JVM является важным компонентом JRE и представляет собой
абстрактную вычислительную машину с набором команд,использующую память.
Основное назначение JVM - позволить Java приложениям выполняться
на любой платформе.JVM имеет собственную реализацию для каждой операционной
системы.
JVM включает интерпретатор Java,компилятор времени выполнения
JIT (Just in time),сборщик мусора(garbage collection) и блок
синхронизации потоков.Интерпретатор Java преобразует последовательно
байткоды в вызовы операционной системы.
JIT компилятор преобразует байткоды во внутренний код платформы,на
которой он запускается.За счет компиляции байткода в собственный код платформы
повышается скорость выполнения приложения.Компиляция выполняется для каждого
метода при вызове его в первый раз.Последующие вызовы метода приводят к
выполнению собственного кода.
Новая версия JDK 1.2
Новая версия JDK 1.2 представляет высокопроизводительную JVM,
оптимизированную под Solaris.Поддерживается версиями Solaris
2.6,7.
Хотя одним из преимуществ языка Java является его платформо-независимость,на
производительность Java приложений влияют особенности используемой
платформы. Использование собственной многопоточной платформы Solaris(Solaris
Native Thread) позволяет осуществлять диспетчеризацию потоков между
процессорами. Все интерпретаторы Java обеспечивают многопоточность,однако
многие из них поддерживают только однопроцессорную многопоточность,т.е.
только один поток выполняется в каждый момент времени.Прежние реализации
Java (до версии
Solaris 2.6) использовали пользовательскую
модель потоков,называемую "green threads" и являющуюся частью JRE.Данная
библиотека позволяла реализовать модель Many-to-One (много пользовательских
потоков а один поток ядра).В каждый момент времени могла выполняться только
одна "green thread". Позтому Java приложения не могли запускаться
параллельно на нескольких процессорах вместе с другими многопоточными приложениями
Solaris.
Solaris Native Thread реализует 2-х уровневую многопоточную
модель
Many-to-many (много пользовательских потоков в много потоков
ядра).Ядро управляет только активными потоками.Использование данной модели
снимает ограничение на количество потоков,которые могут эффективно запускаться
приложением.
JDK 1.2 обеспечивает :
-
улучшенную систему распределения памяти и сборки мусора
-
уменьшенные накладные расходы по синхронизации потоков
-
более тонкий механизм блокировок
-
оптимизированный JIT компилятор
-
улучшенные библиотеки классов и новый Java 2 API
-
более эффективную реализацию операций с плавающей точкой
-
дополнительные возможности отладчика
Испольэование JDK 1.2 позволяет значительно увеличить производительность
Java
приложений для серверов.
Инструментальные средства JDK
Для разработки Java приложений имeется набор инструментальных средств
JDK,включающий следующие основные компоненты :
- компилятор javac
- интерпретатор java
- средство просмотра аплетов appletviewer
- архиватор jar
- отладчик jdb
- дизассемблер файлов классов javap
- генератор С файлов javah
- генератор документации javadoc
- исходный код интерфейса API
- примеры аплетов
Компилятор javac предназначен для компиляции исходных текстов
Java
приложений(с расширением .java) в байткоды исполняемых классов
(с расширением .class).Компилятор написан на языке Java
и генерирует отдельный файл для каждого класса,определенного в файле исходного
текста.
Компилятор требует,чтобы в одном файле исходного текста был определен
только один public class и чтобы имя файла(без расширения
.java)
совпадало с именем класса.
Динамический интерпретатор java обеспечивает поддержку
выполнения программ на Java в скомпилированном формате байткода.В
командной строке запуска имя класса указывается без расширения .class.Указанный
класс должен содержать метод main().Если в методе main()
создаются потоки,то java выполняется до тех пор,пока не завершится
последний поток.
Интерпретатор
java автоматически загружает все дополнительные
классы,необходимые для выполнения приложения.Для запуска java
в режиме отладки используется параметр -debug ,что дает возможность
использовать отладчик
jdb совместно с интерпретатором.
Программа appletviewer является автономным средством просмотра
аплетов вместо Web-браузера.Загружает один или несколько HTML документов,
запуская при этом все аплеты,на которые содержатся ссылки и отображая каждый
из них в собственном окне.
Для запуска в режиме отладки используется параметр -debug.
Архиватор jar используется для создания архивных файлов
Java(JAR) и работы с ними.JAR-файл представляет собой сжатый ZIP-файл с
дополнительным файлом описания.Синтаксис jar напоминает синтаксис
tar
OC UNIX.
Отладчик jdb предназначен для отладки Java приложений.При
вызове
jdb с именем класса Java запускается другая
копия интерпретатора java. Эта новая копия загружает указанный
файл класса и прерывает его выполнение, ожидая ввода команд отладки.
Синтаксис jdb аналогичен синтаксису UNIX dbx, gdb.
Дизассемблер javap дизассемблирует файлы классов,имена
которых указаны в командной строке,и выводит их тексты в доступном для
чтения виде. Его использование полезно,когда для данного класса отсутствует
исходный код.
Генератор С файлов javah служит для связывания
внешних и библиотечных функций,написанных на С,и Java приложений.Генератор
создает файлы заголовков и заглушек(фиктивных модулей,содержащих код,необходимый
для взаимодействия внешнего метода и исполняющей системы Java),описывыющих
указанные классы Java.Генерируется структура на С ,соответствующая
той,что используется в классе Java.Генерируемые файлы содержат информацию,
необходимую для реализации методов указанных классов на С.
Синтаксис вызова следующий :
javah [параметры] имена_классов
По умолчанию javah создает файл заголовков для указанного
класса (с расширением .h).Данный файл должен быть включен при
помощи оператора
include в реализацию внешней С функции.
Если задан параметр -stubs ,создается файл с расширением .с
,содержащий дополнительные процедуры-заглушки, необходимые для связи собственного
(платформо-зависимого) метода со средой
Java.В созданный файл не
следует помещать код реализации внешнего метода.
Если задан параметр -jni (Java Native Interface),javah
создает файл прототипов платформо-зависимой реализации методов указанног
класса.Результат помещается в файл с расширением .h.При использовании
собственного интерфейса (-jni) не требуется генерирование файлов
заголовков и заглушек и потому параметр -stub не может использоваться
в сочетании с параметром
-jni.
Так как код внешнего метода не ограничен исполняющей системой Java,он
представляет угрозу нарушения защиты.Поэтому аплеты не могут использовать
внешние методы.Кроме того,приложения использующие внешние методы,становятся
непереносимыми.
Генератор документации javadoc создает документацию по
API в формате HTML для указанных пакетов или файлов исходных текстов Java.По
умолчанию HTML-файлы создаются в текущем каталоге.Изменить каталог можно
опцией -d dir.Файлы документации классов,создаваемые
javadoc,описывают
класс и его иерархию наследования,члены класса,конструкторы,методы.
Интерфейс API Java представляет собой набор классов,используемых
для разработки Java приложений.Классы организованы в группы,называемые
пакетами(packages).
|