Команды создания ссылочных данных и доступа к ним, в том числе:
создания экземпляров класса;
доступа к полям класса;
создания массивов;
чтения в стек и сохранения элементов массивов;
получения свойств массивов и объектов.
Команды прямого манипулирования со стеком.
Команды передачи управления, в том числе:
безусловный переход;
условный переход;
переход по множественному выбору.
Команды вызова методов и возврата (включая специальные команды вызова синхронизированных методов).
Команды генерации и обработки исключений.
Принятые в спецификациях Java VM структуры данных и алгоритмы таковы, что позволяют реализовать виртуальную машину с минимальными затратами памяти и сделать ее работу максимально эффективной.
Другим ключевым элементом спецификаций Java является файл класса. Каждый файл класса описывает один класс или интерфейс. Файл класса содержит поток байт, структурированный определенным образом. Все реализации компилятора Java должны генерировать файлы классов, структура которых соответствует определенной в спецификациях. Все реализации Java VM должны "понимать" структуру файлы класса, соответствующую определенной в спецификациях.
Основные компоненты файла класса следующие:
Некоторая верификационная информация: "магическое число" - сигнатура файла класса, номер версии.
Флаг доступа, отображающий модификаторы, заданные в определении класса (public, final, abstract и т.д.), а также признак класса или интерфейса.
Пул констант - таблица структур, представляющих различные строковые константы - имена классов и интерфейсов, полей, методов и другие константы, на которые есть ссылки в файле класса.
Ссылки на имена this-класса и суперкласса в пуле констант.
Перечень интерфейсов, реализуемых классом (в виде ссылок в пул констант).
Описание полей класса с указанием их имен, типов, модификаторов и т.д.
Методы класса - каждый метод представляется в виде определенной структуры, в которой содержится описание метода (имя, модификаторы, и т.д.), одним из атрибутов этой структуры является массив байт-кодов метода.
Многие компоненты файла класса (пул констант, перечень интерфейсов и др.) имеют нефиксированную длину, такие компоненты предваряются 2-байтным полем, содержащим их длину.
13.3 Многопоточность и синхронизация
Java, по-видимому, является единственным универсальным языком программирования, в котором механизмы создания нитей поддерживаются встроенными средствами языка. В традиционных языках программирования (например, C) создание нитей обеспечивается системно-зависимыми библиотеками, обеспечивающими API ОС. В Java средства создания нитей системно-независимые.
В Java-программе нить представляет отдельный класс, который может быть создан
либо как подкласс (наследник) суперкласса Tread;
либо как класс, реализующий интерфейс Runnable, внутри этого класса должна быть переменная экземпляра класса - ссылка на объект класса Tread.
Суперкласс Tread и интерфейс Runnable определены в базовой библиотеке языка Java - пакете java.lang. При любом варианте создания в классе-нити должен быть реализован метод run(). Выполнение метода start() для экземпляра такого класса вызывает выполнения метода run() в отдельном потоке вычисления.
Как мы увидели в предыдущем разделе, Java VM обеспечивает для каждой нити собственную среду вычисления - собственный набор регистров и стек (в некоторых реализациях Java VM обеспечивает для нити также и собственную кучу).
Но Java VM не выполняет действий по планированию нитей на выполнение. Для этого библиотечные методы Java обращаются к ОС, используя API той ОС, в среде которой работает Java VM.
Для нитей в Java предусмотрено управление приоритетами (методы getPriority(), setPriority()), однако, и здесь Java использует механизмы управления приоритетами ОС. Так, уже классическим для учебников по Java является пример аплета, в котором по экрану "наперегонки" движутся несколько объектов, движение каждого осуществляется в отдельной нити и с собственным приоритетом. Этот пример весьма наглядно демонстрируется в средах, например, OS/2 и Linux, но выглядит не очень убедительным в среде Windows 95/98, так как приоритет нити не слишком влияет на скорость движения объекта - примерно так, как показано на рисунке 13.5.
Рисунок 13.5 "Гонки" в разных операционных средах (движение справа налево).
Любая нить может быть сделана нитью-"демоном". Нить-"демон" продолжает выполняться даже после окончания той нити, в которой она была создана. Нить становится "демоном" при ее создании только в том случае, если она создается из нити-"демона". Программа может изменить состояния нити при помощи метода setDaemon() класса Thread. Выполнение любой программы Java VM начинает с единственной нити (в которой вызывается метод main()), и эта нить запускается не как "демон". Java VM продолжает существовать, пока не завершатся все нити не-"демоны".
В языке Java имеется также класс ThreadGroup - группа нитей, которая может управляться совместно.
Если в Java предусмотрены нити, то, естественно, должны быть предусмотрены и средства синхронизации и взаимного исключения при параллельной работе нитей. Основным средством синхронизации и взаимного исключения в Java является ключевое слово synchronized, которое может употребляться перед каким-либо программным блоком. Ключевое слово synchronized определяет невозможность использования программного блока двумя или более нитей одновременно. В Java synchronized-блоки называются мониторами и их фактическая тождественность мониторам Хоара, описанным в разделе 8.8 части I, очевидна. В зависимости от деталей способа употребления synchronized может работать как:
защищенная (guard - см. раздел 8.8 части I) процедура - в том случае, если synchronized-блок представляет собой целый метод, однако, в отличие от описанных нами guard-процедур одновременное вхождение в разные synchronized-методы возможно;
анонимные скобки критической секции - в том случае, если synchronized-блок является просто программным блоком;
скобки критической секции с защитой выбранного ресурса - в том случае, если после ключевого слова synchronized указывается в скобках ссылка на объект.
В первоначальной версии языка Java для класса Thread предусмотрены методы:
resume() - приостановить выполнение нити;
suspend() - возобновить выполнение нити;
yeld() - сделать паузу в выполнении нити, чтобы дать возможность выполниться другой нити;
join() - ожидать завершения нити.
Эти средства позволяют синхронизировать работу нитей, но в следующих версиях был (наряду со старыми средствами) введен новый, более стройный аппарат синхронизации и взаимного исключения.
Класс Object имеет три метода:
wait() - ожидать уведомления об этом объекте;
notify() - послать уведомление одной из нитей, ждущих уведомления об этом объекте;
notifyAll() - послать уведомление всем из нитям, ждущим уведомления об этом объекте.
Поскольку класс Object является корнем иерархии классов, объекты всех - стандартных и пользовательских - классов являются его подклассами и наследуют эти методы. Эти методы аналогичны операциям wait и signal, описанным нами в разделе 8.7 части I. Реализация, например, общего (с возможным значением, большим 1) семафора с использованием этих средств будет выглядеть следующим образом:
//** семафор реализуется в виде класса Semaphore
public class Semaphore
{
// значениесемафора
private int Semaphore_value;
//** пустой конструктор семафора, по умолчанию начальное значение семафора - 0
public Semaphore()
{
this(0);
}
//** конструктор с параметром - начальным значением семафора,
// если задано отрицательное значение, устанавливается начальное значение 0
public Semaphore(int val)
{
if (val < 0) Semaphore_value = 0
else Semaphore_value = val;
}
//** V-операция. V- и P-операции объявлены synchronized,
// чтобы исключить одновременное выполнение их двумя или более нитями
public synchronized void V()
{
// возможно пробуждает нить, ожидающую у семафора
if (Semaphore_value == 0) this.notify();
// увеличивает значение семафора
Semaphore_value++;
}
//** P-операция
public synchronized void P() throws InterruptedException
// исключение может выбрасываться в wait
{
// если значение семафора равно 0, блокирует нить
while (counter == 0) this.wait();
// уменьшает значение семафора
Semaphore_value--;
}
}
В Java VM с каждым объектом связывается замок (lock) и список ожидания (wait set).
В спецификациях байт-кода Java имеются специальные команды monitorenter и monitorexit, устанавливающие и снимающие замок. Java VM, входя в synchronized-блок, пытается выполнить операцию установки замка и не продолжает выполнения нити, пока операция не будет выполнена. При выходе из synchronized-блока выполняется операция снятия замка.
Список ожидания используется методами wait(), notify(), notifyAll(). Он представляет собой список нитей, ожидающих уведомления о данном объекте. Названные операции работают с этим списком очевидным образом.
Следует отметить, что многопоточность, заложенная в языке Java, имеет большие перспективы. Изначально сама идея нитей (а ее первая коммерческая реализация была сделана именно фирмой Sun Microsystems) возникла как решение, призванное обеспечить эффективную загрузку оборудования в многопроцессорных системах, но теперь свойства многопоточности, закладываемые в программное обеспечение, оказывают (наряду с другими факторами) обратное влияние на процессорные архитектуры, стимулируя развитие в них средств распараллеливания вычислительного процесса.
Так, новый проект компьютерной архитектуры фирмы Sun Microsystems носит название MAJC (Microprocessor Architecture for Java Computing - архитектура микропроцессора для Java-вычислений), которое говорит само за себя. В концепцию этой архитектуры (как, впрочем, и ряда других новых архитектур) входит многопроцессорная обработка с несколькими "потоковыми устройствами" в каждом процессоре. Такая архитектура призвана обеспечить более эффективную обработку на сетевом сервере современных потоков данных, которые характеризуются возрастанием удельного веса в них мультимедийной информации.