RTSJ: The Real-Time Specification for Java

RTSJ: The Real-Time Specification for Java

a.k.a. "Write once carefully, run anywhere conditionally"

By Don Busch, OCI Principal Software Engineer and Partner

May 2006


Introduction

The Java platform is not the first platform that comes to mind when thinking of real-time applications. Java's garbage collection, while adding convenience and safety to conventional applications, can play havoc with the determinism and predictability requirements of a typical real-time system. In a real-time application, the right answer delivered after a deadline has passed is no answer at all. Predictability is frequently more important than raw performance.

The Real-Time Specification for Java (RTSJ) aims to change that perception by enhancing the Java platform with capabilities required by real-time applications. The RTSJ team members believe that the Real-Time Specification for Java will be the first real-time programming language to be both technologically and commercially successful. That's a very strong statement which will undoubtedly provoke debate, but it also indicates the scope of the capabilities and enhancements in RTSJ.

The RTSJ specification is the result of JSR-1, and was the first specification launched through the Java Community Process. The current version of the specification isRTSJ 1.0.1(b). RTSJ 1.0.2 is in progress and is currently available in draft form. The next major RTSJ revision, RTSJ 1.1, is JSR-282.

A reference implementation of an RTSJ-compliant Java Virtual Machine is available from TimeSys Linux. The reference implementation does not include a Java compiler, as RTSJ programs are compiled with a conventional Java compiler.

Guiding Principles of RTSJ

The RTSJ expert group followed many guiding principles in developing the RTSJ specification. The team attempted to come as close as possible to meeting Java's "write once, run anywhere" mantra without compromising the capabilities required by real-time systems. However, in determining RTSJ behavior, predictability always takes precedence over binary portability. In other words, an RTSJ-compliant JVM achieves "write once carefully, run anywhere conditionally".

An RTSJ implementation is not restricted to specialized Java environments such as Java Micro Edition or the Embedded Java Application Environment. An RTSJ application is compiled with a standard Java compiler and executes in a special RTSJ virtual machine. The RTSJ virtual machine must also be able to execute standard Java bytecode.

RTSJ does not add any new keywords or syntactic extensions to Java. All RTSJ usage is accomplished through a set of Java classes and interfaces. It does not mandate any particular algorithms (e.g. for thread scheduling) or other implementation decisions, allowing users to strategize those parameters.

This article is a survey of the RTSJ enhancements to the Java platform. It does not intend to provide detailed programming examples of RTSJ usage, but rather to present a flavor of the types of enhancements that the Real-Time Specification for Java provides to be usable in real-time applications.

RTSJ Areas of Enhancement

The RTSJ contains seven "areas of enhancement" where it embellishes the standard Java specification. They are as follows:

    1. Memory Management
    2. Thread Scheduling and Dispatching
    3. Synchronization and Resource Sharing
    4. Asynchronous Event Handling
    5. Asynchronous Transfer of Control
    6. Asynchronous Thread Termination
    7. Physical Memory Access

We will discuss these enhancements in the remainder of this article.

1. Memory Management

Java garbage collection has always been an obstacle to real-time programming due to its unpredictability. RTSJ extends the Java memory model to permit determinism in memory management.

RTSJ memory management is independent of any garbage collection algorithm. RTSJ implementations must allow a program to specify a garbage collection algorithm's effect on thread execution and dispatching, and must allow memory to be allocated and freed outside of garbage collection. To do this, an RTSJ implementation allows a program to allocate memory outside the control of the standard Java garbage-collected heap. RTSJ introduces the concept of a memory area. There are four types of memory areas:

      • Scoped Memory has a lifetime defined by its scope. It is conceptually similar to stack-allocated memory, but its lifetime is more general than that of a stack-allocated object.

      • Physical Memory permits an application to create an object in a specific physical memory region. For example, an application may create objects in a physical memory region that has especially fast memory access.

      • Immortal Memory can be referenced by any schedulable object without exception or garbage collection delay.

      • Heap Memory is simply generic Java heap memory.

An RTSJ memory area is defined by a subclass of the abstract MemoryArea class. The following code snippet is a partial description of the MemoryArea class interface:

  1. package javax.realtime;
  2. public abstract class MemoryArea {
  3. protected MemoryArea(long sizeInBytes);
  4. protected MemoryArea(long sizeInBytes, Runnable logic);
  5. ...
  6. // methods
  7. public void enter();
  8. public void enter(Runnable logic);
  9.  
  10. public static MemoryArea getMemoryArea(Object object);
  11. public long memoryConsumed();
  12. public long memoryRemaining();
  13. public long size();
  14. ...
  15. }

A user of a memory area associates a Runnable algorithm with the memory area either in the memory area's constructor or in its enter() method. All new memory allocations in the algorithm are allocated from the memory area. If space in the memory area is exhausted, then an OutOfMemoryError exception is thrown.

Scoped Memory bounds the lifetime of any object associated with the scope. Once in a scope, any call to new allocates memory from within the scope. A section of code may either enter the scope explicitly with the enter method, or attach a scope to a schedulable object such as a RealtimeThread. The scope is discarded when nothing in it can be referenced.

The assignment of scoped objects is limited. A scoped object cannot be assigned to a reference from an outer scope, to the heap, or to immortal memory.

The scoped memory class, ScopedMemory, is also an abstract class. Its interface is almost identical to its base class's interface.

  1. package javax.realtime;
  2. public abstract class ScopedMemory extends MemoryArea {
  3. // constructors
  4. public ScopedMemory(long size);
  5. public ScopedMemory(long size, Runnable logic);
  6. ...
  7. // methods
  8. public void enter();
  9. public void enter(Runnable logic);
  10. public long getMaximumSize();
  11. ...
  12. }
  13.  

There are two concrete types of scoped memory. Linear-Time (LT) memory consumes a linear amount of time relative to the amount of memory requested for each memory allocation. Linear Time does not refer to the time it takes to run the object's constructor.

  1. package javax.realtime;
  2. public class LTMemory extends ScopedMemory {
  3. //constructors
  4. public LTMemory(long initialSizeInBytes,
  5. long maxSizeInBytes);
  6. public LTMemory(long initialSizeInBytes,
  7. long maxSizeInBytes, Runnable logic);
  8. ...
  9. // methods
  10. public int getMaximumSize();
  11. }

Variable-Time (VT) Memory is memory for which it may take a variable amount of time for any particular allocation. The time required to allocate a chunk of memory is not as predictable as with Linear Time memory.

  1. package javax.realtime;
  2. public class VTMemory extends ScopedMemory {
  3. //constructors
  4. public VTMemory(long initialSizeInBytes,
  5. long maxSizeInBytes);
  6. public VTMemory(long initialSizeInBytes,
  7. long maxSizeInBytes, Runnable logic);
  8. ...
  9. // methods
  10. public int getMaximumSize();
  11. }

Each scope is reference-counted. The count is incremented on each enter() call and decremented each time an associated Runnable.run() method completes. Upon reaching 0, the scope is deleted and finalization code is run on each associated object. For example:

  1. javax.realtime.MemoryArea mem =
  2. new javax.realtime.LTMemory( 8192, 32768 );
  3.  
  4. mem.enter(new Runnable()
  5. {
  6. public void run() {
  7. // all memory allocations in this algorithm
  8. // occur in the LTMemory memory area.
  9. }
  10. } );

In this simple example, the scope is discarded at the end of the logic in the run() method.

Physical Memory access is enabled through the RawMemoryAccess class. It enables an RTSJ application to access memory at a particular fixed memory address for performance or other reasons.

Physical memory is simply a fixed sequence of bytes. Such memory can be mapped to device drivers, memory-mapped I/O, battery-backed RAM, etc. Raw memory cannot contain references to other Java objects -- it can only contain basic types.

Immortal Memory is not garbage collected. It can be referenced by any schedulable object without exception or garbage collection delay.

  1. package javax.realtime;
  2. public final class ImmortalMemory extends MemoryArea {
  3. public static ImmortalMemory instance();
  4. }

A programmer can provide Memory Allocation Budget for the consumption and the maximum allocation rate of any of these memory areas. This is valuable when a program exceeding its budget or rate indicates an error condition.

2. Thread Scheduling and Dispatching

An RTSJ implementation must provide a thread scheduling interface, but cannot mandate a scheduling algorithm. The default scheduler is a preemptive, priority-based scheduler with 28 priorities. It is expected that more specific thread scheduling and dispatching algorithms would be tied to a particular RTSJ implementation.

For a real-time system, predictable execution is critical. The right answer delivered after a deadline has passed is frequently a fault. The fundamental thrust of real-time programming is ensuring that a program's threads will always complete execution within their deadlines. RTSJ uses the concept of a "schedulable object", adding the RealtimeThread class for thread management. The RealtimeThread class implements the RTSJ Schedulable interface, which we won't show.

  1. package javax.realtime;
  2. public class RealtimeThread extends Thread
  3. implements Schedulable
  4. {
  5. // constructors
  6. public RealtimeThread();
  7. public RealtimeThread(SchedulingParameters scheduling);
  8. public RealtimeThread(SchedulingParameters scheduling,
  9. ReleaseParameters release);
  10. public RealtimeThread(SchedulingParameters scheduling,
  11. ReleaseParameters release,
  12. MemoryParameters memory,
  13. MemoryArea area,
  14. ProcessingGroupParameters group,
  15. Runnable logic);
  16. ...
  17.  
  18. public static MemoryArea getCurrentMemoryArea();
  19. public MemoryArea getMemoryArea();
  20. ...
  21.  
  22. public void interrupt();
  23. public static void sleep(Clock clock, HighResolutionTime time)
  24. throws InterruptedException;
  25. public static void sleep(HighResolutionTime time)
  26. throws InterruptedException;
  27.  
  28. public void start();
  29. public static RealtimeThread currentRealtimeThread();
  30. ...
  31. }

The RTSJ scheduling facility must be flexible enough to permit arbitrary scheduling algorithms, including platform-dependent (i.e. non-portable) schedulers. As said earlier, "write once carefully, run anywhere conditionally". The base Scheduler class is an abstract class:

  1. package javax.realtime;
  2. public abstract class Scheduler {
  3. protected Scheduler();
  4. ...
  5. public abstract boolean setIfFeasible( Schedulable schedulable,
  6. ReleaseParameters release,
  7. MemoryParameters memory );
  8. protected abstract boolean addToFeasibility( Schedulable schedulable );
  9. public abstract boolean isFeasible();
  10. protected abstract boolean removeFromFeasibility( Schedulable schedulable );
  11.  
  12. ...
  13. public abstract void fireSchedulable( Schedulable schedulable);
  14.  
  15. public static Scheduler getDefaultScheduler();
  16. public abstract String getPolicyName();
  17. public static void setDefaultScheduler( Scheduler scheduler);
  18. ...
  19. }

The Priority Scheduler, not surprisingly, schedules the schedulable objects based on their priority, implementing priority-based preemptive dispatching. In other words, the highest priority schedulable object is always assigned to the processing resource. A thread's priority can be changed at runtime.

  1. public class PriorityScheduler extends Scheduler
  2. {
  3. public static final int MAX_PRIORITY;
  4. public static final int MIN_PRIORITY;
  5.  
  6. // constructors
  7. protected PriorityScheduler();
  8.  
  9. protected boolean addToFeasibility(Schedulable schedulable);
  10. public boolean isFeasible();
  11. protected boolean removeFromFeasibility(Schedulable schedulable);
  12. public boolean setIfFeasible(Schedulable schedulable,
  13. ReleaseParameters release,
  14. MemoryParameters memory);
  15. ...
  16. }

3. Synchronization and Resource Sharing

RTSJ changes the behavior of Java's synchronization wait queues. Threads and asynchronous handlers waiting for a resource (e.g. a mutex lock) are released in the order that they are eligible to execute; in other words, the highest priority thread is released first. If there are two threads of the same priority waiting to execute, the threads are released in FIFO order. Standard Java makes no promises of the order of thread release.

The RTSJ requires that a compliant JVM implement the synchronized keyword without allowing unbounded priority inversion. Priority inversion occurs when a lower priority thread locks a resource that is needed by a higher priority thread; the lower priority thread prevents the higher priority thread from executing.

An RTSJ-compliant JVM usually accomplishes this by implementing the "priority inheritance protocol". If a lower priority thread holds a lock that is needed by a higher priority thread, then the lower priority thread's priority is raised to the same value as the higher priority thread's for as long as the lower priority thread holds the lock.

Another approach is to implement the "priority ceiling algorithm". For more information on this, see the "Design" section of the RTSJ specification, in the subsection titled "Priority Inversion Avoidance".

4. Asynchronous Event Handling

The real world is asynchronous. Real-time applications interact with the real world. Thus real-time applications need a mechanism to handle interaction with the real world, such as getting information from devices, sensors, etc. RTSJ uses asynchronous event handlers to accomplish this. Examples of asynchronous events are hardware interrupts, signals, or computed events.

An asynchronous event handler marries an event or events that can happen with the logic that executes when the event happens. The execution logic of an asynchronous event handler is scheduled and dispatched by the scheduler. When a handler is fired, it looks like the handler has been assigned its own thread.

An event is an instance of the AsyncEvent class.

  1. package javax.realtime;
  2. public class AsyncEvent {
  3. // constructors
  4. public AsyncEvent();
  5. // methods
  6. public void addHandler(AsyncEventHandler handler);
  7. public void removeHandler(AsyncEventHandler handler);
  8. public void setHandler(AsyncEventHandler handler);
  9. public boolean handledBy(AsyncEventHandler target);
  10.  
  11. public void bindTo(String happening)
  12. throws UnknownHappeningException;
  13. public void unBindTo(String happening)
  14. throws UnknownHappeningException;
  15.  
  16. public ReleaseParameters createReleaseParameters();
  17. public void fire();
  18. }
  19.  

An event handler implements the AsyncEventHandler class.

  1. package javax.realtime;
  2. public class AsyncEventHandler implements Schedulable
  3. {
  4. //constructors
  5. public AsyncEventHandler();
  6. public AsyncEventHandler(Runnable logic);
  7. ...
  8.  
  9. public void handleAsyncEvent();
  10. public final void run();
  11. ...
  12. }

The javax.realtime.Timer class is a special type of asynchronous event handler.

5. Asynchronous Transfer of Control

Some algorithms are long and take variable amounts of execution time, but may also contain valuable intermediate results. It can be useful to asynchronously transfer control from the algorithm to another thread that is interested in an intermediate result when a certain amount of time has passed. Other threads that need the algorithm's intermediate results can get them in a timely manner. This is asynchronous transfer of control.

There are several principles which must be followed to use asynchronous transfer of control. A method — usually a long method with an unpredictable completion time — declares that it is susceptible to ATC, meaning that control can be transferred from it. Code segments in the method are marked to be either executed to completion or eligible for asynchronous transfer of control.

Once a transfer of control is triggered from a method, control does not come back to that method. In other words, it is a one-way transfer of control. A two-way transfer can be accomplished by using an asynchronous event handler to transfer control back to the originating algorithm.

An asynchronous transfer of control can be triggered with an external event or a timer. A real-time thread can be aborted via ATC.

When the long-running method is interrupted via ATC, it throws an AsynchronouslyInterruptedException.

6. Asynchronous Real-Time Thread Termination

A real-time application needs to be able to safely stop an executing thread. The standard Java language does not have a way to do this; Thread.stop() is considered to be unsafe, and has been deprecated. Thus, handling all of the conditions that may require a thread to terminate is left to the programmer's application code. But a programmer should not be required to anticipate every real-world condition that may require a thread to exit.

To permit asynchronous termination, RTSJ RealtimeThread instances are interruptible. The RTSJ specification describes in detail the algorithmic idiom used to implement asynchronous thread termination. See the "Asynchronous Real-Time Thread Termination" subsection of the RTSJ specification's "Design" section for more details.

7. Physical Memory Access

Physical memory access is enabled through the RawMemoryAccess class. It enables an RTSJ application to access memory at a particular fixed memory address for performance or other reasons.

Physical memory is simply a fixed sequence of bytes. Such memory can be mapped to device drivers, memory-mapped I/O, battery-backed RAM, etc. Raw memory cannot contain references to other Java objects — it can only contain basic types.

Obtaining an RTSJ Reference Implementation

As mentioned in the introduction, a reference implementation of an RTSJ-compliant Java Virtual Machine is available from TimeSys Linux. The reference implementation does not include a Java compiler, as RTSJ programs are compiled with a conventional Java compiler. The reference implementation requires a Linux operating system and the Java 1.3 or 1.4 compiler. Obviously, a non-real-time Linux operating system won't meet real-time requirements, but it will allow you to compile programs and run them with the RTSJ virtual machine.

Once you unzip the RTSJ Reference Implementation CD image, run the install script in the unzipped archive's top-level directory. The README file in that directory contains installation instructions. The installation script installs the RTSJ JVM in the /opt/timesys directory. You may also need to install the pthreads library, which you can do via

  cd /opt/timesys/rtsj-ri/pthreadrt; make install

Example

Let us run a simple example that creates three "Hello World" threads, each allocating memory from a different type of memory area. The HelloThread class is a real-time thread that prints a short message describing the amount of memory consumed.

  1. import javax.realtime.*;
  2.  
  3. public class HelloThread extends RealtimeThread
  4. {
  5. public HelloThread( MemoryArea mem )
  6. {
  7. super( null, null, null, mem, null, null);
  8. this.setName( mem.getClass().getName() );
  9. }
  10.  
  11. public void run()
  12. {
  13. // memory allocated for string concatenation will come
  14. // from the associated Memory Area
  15. String s = this.getName() + " Hello World";
  16. System.out.println( s );
  17.  
  18. String s2 =
  19. this.getName() + " Consumed "
  20. + this.getMemoryArea().memoryConsumed() + " bytes";
  21. System.out.println( s2 );
  22. }
  23. }

The HelloWorld class spawns three threads using three different memory areas.

  1. import javax.realtime.*;
  2.  
  3. public class HelloWorld
  4. {
  5. public static void main( String[] args )
  6. {
  7. // allocate the HelloWorld string from Immortal Memory
  8. HelloThread t1 = new HelloThread( ImmortalMemory.instance() );
  9.  
  10. // allocate the HelloWorld string from Linear Time Scoped Memory
  11. LTMemory ltmem = new LTMemory( 8192, 32768 );
  12. HelloThread t2 = new HelloThread( ltmem );
  13.  
  14. // allocate the HelloWorld string from Variable Time Scoped Memory
  15. VTMemory vtmem = new VTMemory( 8192, 32768 );
  16. HelloThread t3 = new HelloThread( vtmem );
  17.  
  18. t1.start();
  19. t2.start();
  20. t3.start();
  21.  
  22. System.out.println( "Finished Main Thread" );
  23. }
  24. }

On my Linux system, which has a Mandrake Linux distribution with Linux kernel version 2.6.3-4mdk, I execute the commands below to compile and run the simple RTSJ program. My Java compiler is the Linux J2SE 1.4.1_02 Java compiler.

    javac -classpath .:/opt/timesys/rtsj-ri/lib/foundation.jar \
    HelloThread.java HelloWorld.java

Execute the ulimit -s 32 command before you run your code to limit the stack size for the RTSJ virtual machine. You should compile the code in one window and execute it in another, because the Java compiler won't operate with such a small stack size. The LD_ASSUME_KERNEL environment setting is required because RTSJ cannot yet handle the Native POSIX Thread Library (nptl) for Linux.

In a separate window:

  1. ulimit -s 32
  2. export LD_ASSUME_KERNEL=2.4.1
  3.  
  4. /opt/timesys/rtsj-ri/bin/tjvm \
  5. -Djava.class.path=.:/opt/timesys/rtsj-ri/lib/foundation.jar \
  6. -Xbootclasspath=/opt/timesys/rtsj-ri/lib/foundation.jar HelloWorld

The output is:

  javax.realtime.ImmortalMemory Hello World
  javax.realtime.ImmortalMemory Consumed 161028 bytes
  javax.realtime.LTMemory Hello World
  javax.realtime.LTMemory Consumed 708 bytes
  Finished Main Thread
  javax.realtime.VTMemory Hello World
  javax.realtime.VTMemory Consumed 708 bytes

The JVM's memory is allocated from immortal memory, which is why that number is so much higher.

Summary

We have surveyed RTSJ, the Real-Time Specification for Java. We have discussed the areas in which RTSJ enhances the Java platform. We have obtained a reference implementation of an RTSJ virtual machine and demonstrated a simple example program.

References