Разработка приложений для Java: Часть 1. Oтличительные возможности режима реального времени в Java

Использование Java-расширений реального времени при создании вашего приложения

Расширения реального времени для Java™ (real-time Java) сочетают в себе легкость программирования на языке Java с обеспечением производительности, которая требуется приложениям, работающим в режиме реального времени. Эти расширения предоставляют возможности по организации среды реального времени, не поддерживаемые стандартными виртуальными машинами Java. Эта статья, которая открывает серию из трех частей, описывает некоторые из этих возможностей, а также рассказывает об их применении в целях обеспечения режима реального времени для ваших приложений.

Шон Фоули, разработчик программного обеспечения, IBM

Sean FoleyШон Фоули (Sean Foley) является разработчиком программного обеспечения в лаборатории IBM в Оттаве, которая относится к центру Java-технологий IBM. У него есть степени бакалавра математики университета Queens, а также магистра математики университета Торонто, в котором он занимался исследованиями комбинаторных проблем в теориях графов и проектирования. После окончания университета Шон занимался разработкой приложений для ряда компаний, работающих в областях мобильных телекоммуникаций и встраиваемых процессоров. Он стал членом группы разработчиков IBM в 2002 году, начав работать над встраиваемыми JVM и сопутствующими технологиями, такими как средства статического анализа кода и оптимизаторы скомпилированных приложений. После этого он был одним из основных участников создания библиотеки классов реального времени для проекта IBM WebSphere Real Time. В настоящее время он является техническим руководителем группы разработчиков, занимающейся развитием технологий реального времени для Java.



13.09.2010

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

Виртуальные машины Java (JVM), поддерживающие эти расширения, могут использовать сервисы операционных систем реального времени (real-time operating system — RTOS) для обеспечения жесткого режима реального времени. В то же время они могут работать на обычных операционных системах для выполнения приложений, которым достаточно мягкого режима реального времени. Некоторые технологии, используемые расширениями, становятся доступными автоматически при переносе приложения в поддерживающую их JVM. Для использования других технологий необходимо менять код приложения. Именно они будут подробно рассмотрены в этой статье.

В процессе работы приложения JVM выполняет различные сервисные функции, над которыми само приложение, как правило, не имеет непосредственного контроля. Эти функции выполняют ряд дочерних процессов JVM, в числе которых:

  • Сборка мусора. Данный сервис заключается в освобождении выделенных блоков памяти, которые более не требуются приложению. Процесс сбора подобных блоков может приводить к задержкам при работе приложения.
  • Загрузка классов. Этот процесс, называющийся так потому, что Java-приложения загружаются в виде набора классов, включает в себя загрузку структур данных, инструкций и других ресурсов, находящихся в локальной файловой системе или на удаленных хостах. Как правило, приложения загружают каждый класс в момент первого к нему обращения (этот принцип называется ленивой загрузкой - lazy loading).
  • Компиляция на этапе выполнения (just-in-time). Многие виртуальные машины динамически компилируют байт-код Java-методов в наборы машинных инструкций в процессе работы приложения. В целом этот процесс позволяет повысить быстродействие, однако компиляция может приводить к временным задержкам, блокируя выполнение приложения.
  • Планирование выполнения. Как правило, стандартные JVM предоставляют приложениям минимальный набор возможностей по планированию выполнения их потоков, а также глобальному планированию в контексте других процессов, параллельно выполняющихся в той же операционной системе.

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

Расширения реального времени для Java предоставляют ряд технологий, направленных на минимизацию влияния на приложение со стороны подобных служебных процессов. Некоторые возможности становятся доступными автоматически при переходе на JVM, поддерживающие эти расширения. К ним относятся: специализированный сборщик мусора, который ограничивает длительность прерываний, связанных с освобождением памяти, специализированный загрузчик классов, отличающийся энергичной, а не ленивой, загрузкой классов, специализированные реализации блокировок и синхронизации, а также специализированный планировщик потоков, позволяющий избежать эффекта инверсии приоритетов. Однако при этом необходимо вносить определенные изменения в код самого приложения, в частности, для использования функций, предусмотренных спецификацией расширений реального времени для Java (Real-Time Specification for Java – RTSJ).

RTSJ предоставляет API, включающий множество возможностей по поддержке режима реального времени внутри JVM. Некоторые из них являются обязательными для реализации, другие – нет. Они относятся к нескольким крупным аспектам поддержки режима реального времени, перечисленным ниже:

  • планировка выполнения в режиме реального времени;
  • специализированное управление памятью;
  • таймеры высокого разрешения;
  • асинхронная обработка событий;
  • асинхронные прерывания выполнения потоков.

Потоки Realtime

В RTSJ определен класс javax.realtime.RealtimeThread , унаследованный от стандартного класса java.lang.Thread и самостоятельно реализующий некоторые из функций, описанных в спецификации. Например, выполнением потоков-экземпляров этого класса управляет планировщик реального времени. Он предоставляет ряд уникальных возможностей по управлению приоритетами, в том числе планировку реального времени по принципу FIFO (first-in, first-out), которая гарантирует, что потоки с наивысшим приоритетом будут выполняться без прерываний. Планировщик также поддерживает возможность наследования приоритетов (priority inheritance) – алгоритм, позволяющий исключить ситуацию, при которой низкоприоритетные потоки захватывают и неограниченно долго не отпускают блокировку ресурса, требующегося высокоприоритетному процессу, который в противном случае мог бы выполняться беспрепятственно (эта ситуация получила название инверсии приоритетов).

Несмотря на то, что вы можете явным образом создавать экземпляры класса RealtimeThread, существует возможность свести изменения вашего приложения, направленные на использование потоков реального времени, к минимуму, как показано на примерах ниже (исходный код всех примеров можно загрузить по ссылке в разделе Загрузка). Это позволит существенно облегчить и удешевить разработку. Таким образом, вы можете реализовать выполнение потоков вашего приложения в режиме реального времени с минимальными усилиями, а само приложение сохранит совместимость со стандартными JVM.

Выбор типа потоков в зависимости от приоритета

В листинге 1 показан фрагмент кода, который выбирает между обычными потоками и потоками реального времени в зависимости от приоритета. Если приложение выполняет в JVM поддерживающие расширения, то некоторые потоки могут выполняться в режиме реального времени.

Листинг 1. Выбор класса потока на основе приоритета
import javax.realtime.PriorityScheduler;
import javax.realtime.RealtimeThread;
import javax.realtime.Scheduler;

public class ThreadLogic implements Runnable {
    static void startThread(int priority) {
        Thread thread = ThreadAssigner.assignThread(
                priority, new ThreadLogic());
        thread.start();
    }

    public void run() {
        System.out.println("Running " + Thread.currentThread());
    }
}

class ThreadAssigner {
    static Thread assignThread(int priority, Runnable runnable) {
        Thread thread = null;
        if(priority <= Thread.MAX_PRIORITY) {
            thread = new Thread(runnable);
        } else {
            try {
                thread = RTThreadAssigner.assignRTThread(priority, runnable);
            } catch(LinkageError e) {}
            if(thread == null) {
                priority = Thread.MAX_PRIORITY;
                thread = new Thread(runnable);
            }
        }
        thread.setPriority(priority);
        return thread;
    }
}

class RTThreadAssigner {
    static Thread assignRTThread(int priority, Runnable runnable) {
        Scheduler defScheduler = Scheduler.getDefaultScheduler();
        PriorityScheduler scheduler = (PriorityScheduler) defScheduler;
        if(priority >= scheduler.getMinPriority()) {
            return new RealtimeThread(
                    null, null, null, null, null, runnable);
        }
        return null;
    }
}

Для компиляции кода, приведенного в листинге 1, требуются классы из RTSJ. Однако если эти классы не найдены на этапе выполнения, то виртуальная машина сгенерирует исключение типа LinkageError, а затем создаст экземпляры стандартных потоков Java. Таким образом, код может выполняться в любой JVM, а не только в тех, которые реализуют RTSJ.

Обратите внимание, что метод, создающий экземпляры класса RealtimeThread, вынесен в листинге 1 в отдельный класс. Таким образом, он не проходит верификацию до момента загрузки класса, которая выполняется в момент первого вызова метода assignRTThread. В момент загрузки класса верификатор байт-кода внутри JVM пытается проверить, что класс RealtimeThread является наследником Thread, что приведет к ошибке типа NoClassDefFoundError, если классы RTSJ не найдены.

Выбор типа потока при помощи механизма рефлексии

В листинге 2 приведен альтернативный подход для решения той же задачи. В нем также используется приоритет для выбора типа потока (обычный или реального времени), однако инстанциирование выполняется на основе имени класса. Код, использующий механизм рефлексии, будет выполнен успешно в случае наличия конструктора инстанциируемого класса, принимающего экземпляр типа java.lang.Runnable в качестве последнего аргумента. В качестве других аргументов передается пустое значение.

Листинг 2. Использование механизма рефлексии для выбора типа потока
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ThreadLogic implements Runnable {
    static void startThread(int priority) {
        Thread thread = ThreadAssigner.assignThread(
                priority, new ThreadLogic());
        thread.start();
    }

    public void run() {
        System.out.println("Running " + Thread.currentThread());
    }
}

class ThreadAssigner {
    static Thread assignThread(int priority, Runnable runnable) {
        Thread thread = null;
        try {
            thread = assignThread(priority <= Thread.MAX_PRIORITY, runnable);
        } catch(InvocationTargetException e) {
        } catch(IllegalAccessException e) {
        } catch(InstantiationException e) {
        } catch(ClassNotFoundException e) {
        }
        if(thread == null) {
            thread = new Thread(runnable);
            priority = Math.min(priority, Thread.MAX_PRIORITY);
        }
        thread.setPriority(priority);
        return thread;
    }

    static Thread assignThread(boolean regular, Runnable runnable)
        throws InvocationTargetException, IllegalAccessException,
            InstantiationException, ClassNotFoundException {
        Thread thread = assignThread(
                regular ? "java.lang.Thread" : 
                "javax.realtime.RealtimeThread", runnable);
        return thread;
    }

    static Thread assignThread(String className, Runnable runnable)
        throws InvocationTargetException, IllegalAccessException,
            InstantiationException, ClassNotFoundException {
        Class clazz = Class.forName(className);
        Constructor selectedConstructor = null;
        Constructor constructors[] = clazz.getConstructors();
        top:
        for(Constructor constructor : constructors) {
            Class parameterTypes[] =
                constructor.getParameterTypes();
            int parameterTypesLength = parameterTypes.length;
            if(parameterTypesLength == 0) {
                continue;
            }
            Class lastParameter =
                parameterTypes[parameterTypesLength - 1];
            if(lastParameter.equals(Runnable.class)) {
                for(Class parameter : parameterTypes) {
                    if(parameter.isPrimitive()) {
                        continue top;
                    }
                }
                if(selectedConstructor == null ||
                    selectedConstructor.getParameterTypes().length
                        > parameterTypesLength) {
                    selectedConstructor = constructor;
                }
            }
        }
        if(selectedConstructor == null) {
            throw new InstantiationException(
                    "no compatible constructor");
        }
        Class parameterTypes[] =
            selectedConstructor.getParameterTypes();
        int parameterTypesLength = parameterTypes.length;
        Object arguments[] = new Object[parameterTypesLength];
        arguments[parameterTypesLength - 1] = runnable;
        return (Thread) selectedConstructor.newInstance(arguments);
    }
}

Фрагмент кода, показанный в листинге 2, может компилироваться без наличия классов RTSJ в classpath, поскольку потоки реального времени создаются при помощи механизма рефлексии.

Выбор типа потока при помощи наследования классов

В следующем примере демонстрируется, как можно создавать потоки реального времени, подставляя соответствующий родительский тип для заданного класса. Другими словами, вы можете создать две версии класса потоков, один из которых использует javax.realtime.RealtimeThread, а другой – нет. При этом выбор одного из них может зависеть от JVM, в которой будет выполняться приложение. Для того чтобы сделать выбор в пользу того или иного варианта, достаточно просто включить соответствующий класс в дистрибутив приложения. В любом случае код останется достаточно простым, в частности, ему не требуется обработка исключений в отличие  от примеров, приведенных выше. Однако неудобство заключается в том, что вам придется поставлять две версии дистрибутива приложения, включая в них ту версию класса, которая поддерживается виртуальной машиной.

Потоки в листинге 3 создаются стандартным образом.

Листинг 3. Использование наследования для выбора класса потока
import javax.realtime.PriorityScheduler;
import javax.realtime.RealtimeThread;
import javax.realtime.Scheduler;

public class ThreadLogic implements Runnable {
    static void startThread(int priority) {
        ThreadContainerBase base = new ThreadContainer(priority, new ThreadLogic());
        Thread thread = base.thread;
        thread.start();
    }

    public void run() {
        System.out.println("Running " + Thread.currentThread());
    }
}

class ThreadContainer extends ThreadContainerBase {
    ThreadContainer(int priority, Runnable runnable) {
        super(new Thread(runnable));
        if(priority > Thread.MAX_PRIORITY) {
            priority = Thread.MAX_PRIORITY;
        }
        thread.setPriority(priority);
    }
}

class ThreadContainerBase {
    final Thread thread;

    ThreadContainerBase(Thread thread) {
        this.thread = thread;
    }
}

Для использования потоков реального времени достаточно заменить класс ThreadContainer на класс, приведенный в листинге 4.

Листинг 4. Специальный контейнер потоков для выполнения в режиме реального времени
class ThreadContainer extends ThreadContainerBase {
    ThreadContainer(int priority, Runnable runnable) {
        super(assignRTThread(priority, runnable));
        thread.setPriority(priority);
    }

    static Thread assignRTThread(int priority, Runnable runnable) {
        Scheduler defScheduler = Scheduler.getDefaultScheduler();
        PriorityScheduler scheduler = (PriorityScheduler) defScheduler;
        if(priority >= scheduler.getMinPriority()) {
            return new RealtimeThread(
                    null, null, null, null, null, runnable);
        }
        return new Thread(runnable);
    }
}

Таким образом, приложение, предназначенное для выполнения в JVM, поддерживающей расширения реального времени, должно включать скомпилированный класс ThreadContainer, показанный в листинге 4, вместо того, который показан в листинге 3.


Сегментация памяти

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

RTSJ включает в себя понятие контекста выделения (allocation context), а также зоны памяти для каждого потока. Если некоторая зона выступает в качестве контекста выделения для определенного потока, то все его классы инстанциируются в этой области. В спецификации определены следующие зоны памяти:

  • Единственная зона кучи (heap).
  • Единственная зона памяти с неограниченным временем жизни (immortal memory). Память, выделенная в этой зоне, никогда не используется повторно. Она выступает в качестве контекста выделения для потока, выполняющего статический инициализатор какого-либо класса. Объекты, созданные в этой зоне памяти, не освобождаются сборщиком мусора, поэтому ее использование ограничено.
  • Зоны памяти с ограниченным временем жизни (области видимости или просто области). Память, выделенная в этих областях, освобождается без участия сборщика мусора в тот момент, когда виртуальная машина определяет, что они более не являются контекстом выделения ни для одного из активных потоков. При этом уничтожаются все созданные в этих областях объекты, а память становится доступной для повторного использования.
  • Зоны физической памяти, идентифицируемые по их типу или адресу. Разработчики могут помечать каждую такую зону как имеющую ограниченную видимость или неограниченное время жизни. Эти зоны могут предоставлять доступ к памяти, имеющей специфические характеристики или относящейся к определенным видам устройств, например flash-память или разделяемая память.

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

Области памяти и правила присваивания показаны на рисунке 1.

Рисунок 1. Зоны памяти и правила присваивания ссылок на объекты
Memory areas

Необходимо отметить, что правила присваивания не запрещают связи между объектами, находящимися в разных областях. Однако это означает, что области должны освобождаться в строго определенной последовательности, которая хранится в стеке внутри каждого потока. Этот стек также содержит ссылки на другие зоны памяти помимо областей с ограниченной видимостью. Каждая зона памяти, которая становится контекстом выделения для потока, помещается на вершину стека. Правила присваивания разрешают объектам содержать ссылки только на те объекты, которые созданы в областях, находящихся ниже в стеке, чем их собственные области. Это объясняется тем, что чем ближе область памяти к вершине стека, тем скорее она будет освобождена. Ссылки на объекты, находящиеся в вышележащих областях памяти, запрещены.

Порядок следования областей в стеке потока координируется с аналогичным порядком в стеке других потоков. Как только область помещается в стек потока, она тут же становится дочерней по отношению к области, находящейся непосредственно под ней (если же таковой нет, то родительской будет специальная первичная область (primordial scope). Пока область остается в стеке некоторого потока, она может быть помещена в стек другого потока только в том случае, если будет сохранена ее родительская область (т.е. ее текущая родительская область в стеке первого потока должна находиться на вершине стека второго потока). Другими словами, каждая область может иметь только одну родительскую область. Это гарантирует, что освобождение памяти может производиться в последовательности, которая не зависит от того, какой поток выполняет уничтожение объектов. Кроме того, это позволяет правилам присваивания обеспечивать согласованность во всех выполняющихся потоках.

Использование изолированных зон памяти

Если вы хотите использовать определенную зону памяти, вы можете либо указать ее в качестве начальной зоны для выполнения потока в момент его создания, либо выполнить вход в зону явным образом, создав экземпляр типа Runnable, который должен использовать ее по умолчанию.

Использование разных зон памяти требует особой осторожности, поскольку оно сопряжено с определенными трудностями и рисками. Во-первых, вам необходимо определиться с размером и числом зон. Во-вторых, в случае использования областей видимости необходимо тщательно продумать порядок их размещения в стеках потоков и аккуратно соблюдать правила присваивания ссылок между объектами.


Способы планирования выполнения критических участков кода

При работе с данными, располагающимися за пределами кучи, можно использовать класс javax.realtime.NoHeapRealtimeThread (NHRT), являющийся наследником javax.realtime.RealtimeThread. С его помощью можно создавать потоки, выполнению которых ни при каких условиях не может помешать сборщик мусора, поскольку они не обращаются к объектам, размещенным в динамической памяти. Попытка нарушения этого ограничения доступа приведет к выбросу исключения типа javax.realtime.MemoryAccessError.

Другой способ заключается в использовании асинхронной обработки событий, благодаря которой вы можете планировать запуск своего кода в качестве обработчика асинхронных или периодических событий (последние инициируются таймером). При этом нет необходимости явным образом планировать выполнение потоков, соответствующих определенным событиям. Виртуальная машина самостоятельно поддерживает общий пул потоков, используя их для выполнения кода асинхронных обработчиков в момент наступления событий. Это позволяет упростить создание приложений реального времени, освобождая разработчиков от проблем управления потоками и памятью.

Способы планирования выполнения кода представлены на диаграмме классов, показанной на рисунке 2.

Рисунок 2. Диаграмма классов, иллюстрирующая варианты планирования выполнения кода
Scheduling options

Диспетчеризация асинхронных обработчиков событий показана на рисунке 3.

Рисунок 3. Диспетчеризация асинхронных обработчиков событий
Asynchronous dispatch

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

Подробное описание характеристик возможных вариантов диспетчеризации приведено в таблице 1.

Таблица 1. Сравнение методов диспетчеризация кода при использовании расширений реального времени в Java
Совместное использование потоковВозможность периодического запускаВозможность обращения к кучеРабота с памятью с неограниченным временем жизниРабота с памятью с ограниченным временем жизниУчитывание крайнего срока запускаВыполнение без помех со стороны сборщика мусора
Стандартный ThreadНетНетДаДаНетНетНет
RealtimeThreadНетДаДаДаДаДаНет
NoHeapRealtimeThreadНетДаНетДаДаДаДа
AsyncEventHandlerДаДа при использовании таймераДаДаДаДаНет
BoundAsyncEventHandlerНетДа при использовании таймераДаДаДаДаНет
AsyncEventHandler, изолированный от кучиДаДа при использовании таймераНетДаДаДаДа
BoundAsyncEventHandler, изолированный от кучиНетДа при использовании таймераНетДаДаДаДа

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

Таблица 2. Сложности, связанные с выполнением потоков реального времени и работой с памятью
СитуацияОписание
Выделение памяти в определенной зонеДля каждой зоны выделяется тот объем памяти, который был запрошен приложением. Запрашивание слишком большого объема памяти означает неэффективность ее использования, а слишком малого — грозит исключением типа OutOfMemoryError. При этом в процессе разработки может меняться не только само приложение, но и дополнительные библиотеки, также расходующие память. Это также может привести к выходу за пределы созданной зоны памяти.
Общий доступ к областям памяти со стороны нескольких потоков

Область памяти, к которой имеют доступ несколько потоков, может на первый взгляд иметь достаточный размер, поскольку память должна освобождаться в периоды отсутствия обращений. Однако не исключен такой вариант использования, при котором в каждый момент времени область является контекстом выделения, как минимум, для одного потока. В результате память не будет освобождаться, что в итоге приведет к ошибке OutOfMemoryError.

Кроме того, возможны временные конфликты потоков в моменты входа и выхода из областей памяти.

Выброс исключений IllegalAssignmentError, MemoryAccessError и IllegalThreadStateExceptionЭти исключения могут неожиданно возникать в результате неаккуратного проектирования кода, после незначительных изменений в логике приложения и синхронизации. Ниже приведены примеры некоторых подобных ситуаций.
  • NHRT неожиданно получает доступ к объекту, располагающемуся в куче, в результате изменений во времени запуска потоков и синхронизации.
  • Ошибка IllegalAssignmentError  может возникнуть при попытке создания объекта в неизвестной зоне памяти, а также если неизвестно, в каком стеке располагается определенная область.
  • Выброс исключения IllegalThreadStateException  происходит при попытке обращения к областям памяти с ограниченным временем жизни со стороны обычных Java-потоков.
  • Использование статических переменных и других вариантов кэширования данных является небезопасным в областях памяти с ограниченным временем жизни из-за правил присваивания. Это также может приводить к ошибке типа IllegalAssignmentError.
Инициализация классовОбычные потоки и потоки реального времени способны инициализировать классы, включающие NHRT, что может привести к ошибке типа MemoryAccessError.
Финальная обработка объектов, имеющих метод finalizeНа последний поток, покидающий область памяти с ограниченным временем жизни, возлагается задача очистки размещенных в ней объектов.
  • Очистка может привести к неожиданным результатам, если метод finalize попробует создать поток.
  • Очистка может привести к взаимной блокировке. Поток, выполняющий очистку, мог предварительно захватить ряд блокировок. Ожидание этих блокировок со стороны других потоков, а также захват дополнительных блокировок в процессе очистки может стать причиной возникновения взаимной блокировки.
Неожиданные задержки NHRTНесмотря на то, что NHRT гарантированно выполняются без непосредственного вмешательства со стороны сборщика мусора, они могут разделять блокировки с другими потоками, ресурсы которых подлежат сбору. Таким образом, сборщик мусора может косвенно повлиять на выполнение NHRT, задержав выполнение другого потока, который владеет блокировкой, ожидаемой NHRT-потоком.

Расширенный пример

Далее рассмотрим пример использования некоторых возможностей расширений, описанных выше. Обратите внимание на листинг 5, в котором представлены классы производителя и потребителя данных. Оба класса реализуют интерфейс Runnable, поэтому могут легко выполняться при помощи любого объекта типа Schedulable.

Листинг 5. Классы производителя и потребителя данных (событий)
class Producer implements Runnable {
    volatile int eventIdentifier;
    final Thread listener;

    Producer(Thread listener) {
        this.listener = listener;
    }

    public void run() {
        LinkedList<Integer> events = getEvents();
        synchronized(listener) {
            listener.notify();
            events.add(++eventIdentifier); //autoboxing creates an Integer object here
        }
    }

    static LinkedList<Integer> getEvents() {
        ScopedMemory memoryArea = (ScopedMemory) RealtimeThread.getCurrentMemoryArea();
        LinkedList<Integer> events =
            (LinkedList<Integer>) memoryArea.getPortal();
        if(events == null) {
            synchronized(memoryArea) {
                if(events == null) {
                    events = new LinkedList<Integer>();
                    memoryArea.setPortal(events);
                }
            }
        }
        return events;
    }
}

class Consumer implements Runnable {
    boolean setConsuming = true;
    volatile boolean isConsuming;

    public void run() {
        Thread currentThread = Thread.currentThread();
        isConsuming = true;
        try {
            LinkedList<Integer> events = Producer.getEvents();
            int lastEventConsumed = 0;
            synchronized(currentThread) {
                while(setConsuming) {
                    while(lastEventConsumed < events.size()) {
                        System.out.print(events.get(lastEventConsumed++) + " ");
                    }
                    currentThread.wait();
                }
            }
        } catch(InterruptedException e) {
        } finally {
            isConsuming = false;
        }
    }
}

Объекты "производитель" и "потребитель", показанные в листинге 5, имеют доступ к очереди событий, представляющей собой последовательность экземпляров java.lang.Integer. В данном примере подразумевается, что контекстом выделения будет область памяти с ограниченным временем жизни, а очередь будет храниться виде портального объекта. Портальными называются объекты, которые могут храниться в динамической области памяти; к таковым не могут относиться значения статических полей и объекты, память под которые выделена в родительской области. Если производитель обнаружит, что очереди не существует, то он создаст ее в тот же момент. В примере также используется пара изменчивых (volatile) переменных, служащих для информирования потоков о генерации и выборке событий из очереди.

В листинге 6 продемонстрировано выполнение объектов из листинга 5.

Листинг 6. Классы потоков
class NoHeapHandler extends AsyncEventHandler {
    final MemoryArea sharedArea;
    final Producer producer;

    NoHeapHandler(
            PriorityScheduler scheduler,
            ScopedMemory sharedArea,
            Producer producer) {
        super(new PriorityParameters(scheduler.getMaxPriority()),
                null, null, null, null, true);
        this.sharedArea = sharedArea;
        this.producer = producer;
    }

    public void handleAsyncEvent() {
        sharedArea.enter(producer);
    }
}

class NoHeapThread extends NoHeapRealtimeThread {
    boolean terminate;
    final MemoryArea sharedArea;
    final Consumer consumer;

    NoHeapThread(
            PriorityScheduler scheduler,
            ScopedMemory sharedArea,
            Consumer consumer) {
        super(new PriorityParameters(scheduler.getNormPriority()),
            RealtimeThread.getCurrentMemoryArea());
        this.sharedArea = sharedArea;
        this.consumer = consumer;
    }

    public synchronized void run() {
        try {
            while(true) {
                if(consumer.setConsuming) {
                    sharedArea.enter(consumer);
                } else {
                    synchronized(this) {
                        if(!terminate) {
                            if(!consumer.setConsuming) {
                                wait();
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        } catch(InterruptedException e) {}
    }
}

В листинге 6 фрагмент кода, генерирующий данные, помещается в асинхронный обработчик событий, выполняющийся с наивысшим приоритетом. Для выполнения кода генерации данных ему достаточно просто войти в соответствующую область памяти. Та же самая область передается в качестве параметра классу NHRT, который выступает в роли потребителя данных. Его реализация, поддерживающая синхронизированный доступ к полям terminate и setConsuming, также достаточно проста. Этот поток входит в разделяемую область памяти и выполняет код потребителя, однако присваивает ему более низкий приоритет, чем у класса-производителя (в данном примере "потребление" событий заключается в простой печати их идентификаторов в окне консоли).

Фрагмент кода, служащий для инициализации системы и демонстрации ее поведения, приведен в листинге 7.

Листинг 7. Код системы в целом
public class EventSystem implements Runnable {
    public static void main(String args[]) throws InterruptedException {
        RealtimeThread systemThread = new RealtimeThread(
                null, null, null, new VTMemory(20000L), null, null) {
            public void run() {
                VTMemory systemArea = new VTMemory(20000L, new EventSystem());
                systemArea.enter();
            }
        };
        systemThread.start();
    }

    public void run() {
        try {
            PriorityScheduler scheduler =
                (PriorityScheduler) Scheduler.getDefaultScheduler();
            VTMemory scopedArea = new VTMemory(20000L);
            Consumer consumer = new Consumer();
            NoHeapThread thread = new NoHeapThread(scheduler, scopedArea, consumer);
            Producer producer = new Producer(thread);
            NoHeapHandler handler = new NoHeapHandler(scheduler, scopedArea, producer);
            AsyncEvent event = new AsyncEvent();
            event.addHandler(handler);

            int handlerPriority =
                ((PriorityParameters) handler.getSchedulingParameters()).getPriority();
            RealtimeThread.currentRealtimeThread().setPriority(handlerPriority - 1);

            thread.start();
            waitForConsumer(consumer);

            //генерация нескольких событий пока выполняется поток-потребитель
            event.fire();
            event.fire();
            event.fire();
            waitForEvent(producer, 3);

            setConsuming(thread, false);

            //генерация событий после остановки потока-потребителя
            event.fire();
            event.fire();

            waitForEvent(producer, 5);

            setConsuming(thread, true);
            waitForConsumer(consumer);

            //генерация еще одного события при работающем потребителе
            event.fire();
            waitForEvent(producer, 6);

            synchronized(thread) {
                thread.terminate = true;
                setConsuming(thread, false);
            }

        } catch(InterruptedException e) {}
    }

    private void setConsuming(NoHeapThread thread, boolean enabled) {
        synchronized(thread) {
            thread.consumer.setConsuming = enabled;
            thread.notify();
        }
    }

    private void waitForEvent(Producer producer, int eventNumber)
            throws InterruptedException {
        while(producer.eventIdentifier < eventNumber) {
            Thread.sleep(100);
        }
    }

    private void waitForConsumer(Consumer consumer)
            throws InterruptedException {
        while(!consumer.isConsuming) {
            Thread.sleep(100);
        }
    }
}

В листинге 7 в стек помещаются две базовых области памяти, служащие для выполнения обработчика и потока NHRT, поскольку эти экземпляры Schedulable не имеют доступа к объектам, размещенным в куче. Асинхронное событие представляет собой объект, с которым связан обработчик, вызывающийся в моменты наступления события. После инициализации системы код запускает поток потребителя данных, устанавливает для него приоритет на единицу ниже, чем у производителя и трижды генерирует событие. Кроме того, код в примере запускает и останавливает выполнение потока потребителя при наступлении дополнительных событий. 

Результат выполнения класса EventSystem в JVM с расширениями реального времени показан в листинге 8.

Листинг 8. Консольный вывод системы
1 2 3 6

В этом примере определенный интерес представляет тот факт, что события 4 и 5 не выводятся на экран. При этом поток-обработчик просматривает очередь от начала и до конца, поэтому может показаться, что все события должны печататься.

Однако дизайн этого приложения таков, что память, в которой хранятся события, автоматически очищается при отсутствии обращений со стороны потоков. Таким образом, когда поток-потребитель заканчивает чтение данных из очереди, он выходит из области памяти с ограниченным временем жизни, после чего она более не является контекстом выделения ни для одного экземпляра Schedulable.

При отсутствии экземпляров Schedulable, использующих область памяти с ограниченным временем жизни, все хранящиеся в ней объекты уничтожаются. К ним также относится портальный объект, поэтому очередь будет очищена, как только потребитель перестанет отслеживать наступление событий. Возникновение всех последующих событий будет приводить к повторному созданию очереди, однако при отсутствии потоков-потребителей она будет немедленно уничтожена.

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


Общий сценарий использования

Благодаря инфраструктуре планирования потоков и управления памятью вы можете проектировать приложение, использующее различные уровни приоритетов потоков для оптимального выполнения внутри виртуальной машины реального времени (которое также может удовлетворительно работать в других JVM). Приложение может включать высокоприоритетные потоки-обработчики событий, получающие данные от внешних источников и сохраняющие их для дальнейшей обработки. Они имеют ярко выраженную импульсную и асинхронную природу, поэтому для них хорошо подходит специальный подход к управлению памятью. Кроме того, именно на них, как правило, накладываются наиболее жесткие ограничения реального времени. Потоки со средним приоритетом могут заниматься вычислениями, обработкой и распределением данных. Для их работы может потребоваться специальное управление процессорным временем, соответствующее их рабочей нагрузке. Наиболее низкий приоритет могут иметь потоки, выполняющие различные технические функции, например, журналирование. Виртуальные машины реального времени позволяют планировать выполнение этих потоков и управлять их памятью таким образом, чтобы обеспечить максимальную производительность приложения.

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


Заключение к части 1

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

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


Загрузка

ОписаниеИмяРазмер
Исходный код примеров к статьеj-devrtj1.zip5 KБ

Ресурсы

Научиться

  • Оригинал статьи: Real-time Java (Шон Фоули, developerWorks, сентябрь 2009 г.). (EN)
  • Прочитайте шесть статей серии Режим реального времени в Java, рассказывающей о поддержке режима реального времени в Java. (EN)
  • Обратите внимание на видео Гослинг о режиме реального времени в Java (Builder.com), в котором Джеймс Гослинг (James Gosling) опровергает распространенное заблуждение о том, что "режим реального времени" означает сверхпроизводительность. (EN)

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Мобильные приложения
ArticleID=521224
ArticleTitle=Разработка приложений для Java: Часть 1. Oтличительные возможности режима реального времени в Java
publish-date=09132010