This is a short introduction to state machines and how to implement them in Java using the JSpasm package.
An event driven program is one in which the actions of the program are determined by events delivered to the program indicating that certain conditions have arisen. Usually the program runs in a loop; it waits for the next event then when one is delivered it processes it to decide what action to take. Events are usually delivered via a queue and if more events are created while the program is busy processing, the events are held on the queue until the program is ready to accept them.
Most GUI programs written for graphical operating systems are event-driven. Events are used to signal such things as pointer movements, mouse clicks, key presses and "ticks" from the system's internal clock. Event-driven software is found in many other applications from computer games to the firmware that runs in telephone exchanges.
A state machine is a system in which an entity can be in one of a finite (and usually quite small) number of states, and changes to another state (a transition) when an event is delivered to it. The state it changes to depends upon the starting state and the type of event that is delivered. Usually the state machine executes some code associated with the new state when a transition happens.
State machines can make the development of complicated event-driven programs much easier.
A state model is a representation of a related set of states, events and transitions that describes a process. State models can be represented as diagrams or as tables.
JSpasm makes it simple to implement state models as Java classes and to attach them to a state engine that uses the models to control the actions of entity objects in response to events originating inside and outside an application. JSpasm allows one state engine to handle multiple state models in parallel, with multiple entities operating in parallel within each model. The JSpasm engine allows modes of multithreaded operation designed to enhance performance on SMP systems.
The general approach to designing a model is:
As an example we'll consider a very simple system: a push-button on/off switch. Following the steps above:
State models can be implemented in two ways. You can either create an instance of the Model class and call the methods to define the states, events etc., or you can create a subclass of Model and add the definition code to the subclass's constructor. We will use the second approach.
(The complete source code for the following example is available for download at the JSpasm home page.)
Before we define our model we must first create an entity class to use with it. Our model doesn't require any real processing so our entity will be very simple:
public class PushButtonEntity extends AbsEntity { public void offAction() { System.out.println("Switched OFF"); } public void onAction() { System.out.println("Switched ON"); } }
Our model class looks like this:
public class PushButtonModel extends Model { public PushButtonModel(String name) throws Exception { super(name); // Define the entity class setEntityClass(PushButtonEntity.class); // Define states addState("Off", "offAction", ""); addState("On", "onAction", ""); // Set the initial state for new entities setInitialState("Off"); // Define events addEventSpec("push", ""); // Define transitions addNormalTransition("Off", "push", "On"); addNormalTransition("On", "push", "Off"); } }
See the API Javadocs for descriptions of what the method calls do. Note that the order of operations (i.e. define the entity class, states, initial state, events and transitions in that order) is the suggested order.
All that is needed to start the engine is the standard start()
call, just like any other thread. The
Engine
class overrides Thread.start()
adding extra code to ensure that control is not returned
to the calling thread until the engine has initialized itself to the point where it is safe to create entities and
generate events.
To shut the engine down, call shutdown()
. This starts the shutdown sequence, in which the engine shuts down
all internal threads then terminates. Using join()
will ensure that the state machine is completely
quiescent before proceeding.
Note that it can take some time for the engine to shut down; if the shutdown()
is called at a time when
state code that takes a long time to complete is being executed, the shutdown cannot complete until that code has
finished.
Now we can write a section of code that sets up an engine and runs an entity in our model. It's very simple:
// Create a model Model pushButtonModel = new PushButtonModel("MyModel"); // Create an engine Engine engine = new Engine("My Engine"); // Add the model engine.addModel(pushButtonModel); // Start the engine engine.start(); // Now we can create an entity engine.createEntity("MyModel", "MyEntity", new Object[0]); // Push the button a few times engine.generateEvent("MyEntity", "push"); engine.generateEvent("MyEntity", "push"); engine.generateEvent("MyEntity", "push"); engine.generateEvent("MyEntity", "push"); // Wait a couple of seconds Thread.sleep(2000); // Now shut the engine down engine.shutdown(); engine.join();
The output from this code should look like this:
Switched OFF Switched ON Switched OFF Switched ON Switched OFF
Let's take a look at what's happening as the code runs. The first thing that happens is that we create an entity named
"MyEntity" for the model we've named "MyModel". This creates a PushButtonEntity object, because that is the entity class
associated with the PushButtonModel class. The new entity's state is set to "Off" (the initial state defined for the
model) and the method associated with that state - offAction()
- is called for the new entity. This causes
the first line of output.
Now we generate a "push" event for delivery to MyEntity. According to the model's transition definitions, this event in
the Off state causes a transition to the On state. Accordingly, the entity state is changed to On and the associated
method, onAction()
, is called. This causes the second line of the output.
Subsequent "push" events switch the entity alternately between Off and On states, causing a line of output with each transition.
In our example the state action methods require no arguments and so the events that cause transitions into those states
do not need them either. In most models it's likely that you'd want events to carry additional data as arguments to be
passed to the state methods. When defining states and events during building of a model, the arguments expected can be
passed as arrays of Class objects or as comma/space-separated strings. For example consider a state action called
exampleAction()
that expects a String
and an int
as arguments:
public void exampleAction(String s, int i) { ..When defining the state we can supply the expected arguments as a Class array like this:
Class[] args = { String.class, int.class }; addState("example", "exampleAction", args);However, we can use fully-qualified class names in a String, like this:
addState("example", "exampleAction", "java.lang.String, int");The string-based mechanism is probably more convenient in most cases. Note that primitives argument types can be specified in a Class array using
byte.class
, long.class
, etc. In the string form just use the
primitive type name - int
, boolean
etc.
For a no-argument method use a zero-length Class array or the empty string "".
Event arguments are provided as arrays of Objects. Primitive arguments (int
, boolean
, etc.)
are inserted into the array by wrapping them using the wrapper classes (Integer
for int
, etc.).
See the API Javadocs for details.
Consider a model that has three states named A, C and D and three events named e1, e2 and e3:
One way is to add an extra state, B, and set the model to transition into state B when the e2 event is delivered in state A. B's state code performs the extra processing step then generates an e1 event that will cause a transition to state C.
Unfortunately this is likely to cause even more problems. Let's say an e2 event is generated and quickly followed by an e3 event. Now B's state code generates the e1 event needed to transit to state C, but this is behind the e3 event on the event queue. As a result the e3 event is delivered to the entity while it is in state B, and our model doesn't allow for that - we'll get a "can't happen" exception from the engine.
We might be able to redesign the model to cope with events delivered out of sequence like this, but it would be extremely messy. To get around the problem, JSpasm allows internal events. These are high-priority events that are generated by an entity for delivery to itself. They are guaranteed to be delivered before normal-priority events.
In this case, B's code would generate the e1 event as an internal event, and then it doesn't matter whether or not the e3 event is on the queue - the e1 event will always be delivered first and so the problem doesn't happen.
Internal events are generated using the generateInternalEvent()
method, which is a protected method of
the AbsEntity
class.
Our example model uses only NORMAL
transitions. These transitions work exactly the way we've described: the
entity switches state and the end state code is executed.
There are three other transition types:
DO NOT EXECUTE
transitions cause the state change but the end state's code is not executed.
IGNORE
transitions cause no state change and no state code is executed.
EXCURSION
transitions cause the state change and the end state's code is executed in the same way as
for a NORMAL
transition, but the entity's state is restored to the start state after the end state code
has been executed (the start state code is not executed when reverting).
The engine has provision for handling exceptions thrown by state code or internally by the engine during normal running
(for example, when an event is delivered to an entity in a state that has no transition defined for it). By default,
all engines share an instance of BasicExceptionHandler
as the target for exception reports. This handler
simply prints the exception stack trace on standard output.
If your application requires different handling of exceptions (reporting via a Log4J Logger, for example), you can create
a handler class by implementing the IExceptionHandler
interface and then passing an instance of your handler
class to the engine's setExceptionHandler()
method. Exceptions will then be passed to your handler instance
when they occur.
You can also set the exception handler class by specifying a configuration file as a system property. This is described later.
Setting the engine's exception handler to null
will disable reporting of exceptions.
By default the engine does not report state change data, but being able to record state changes can be a very useful
tool when debugging a state model. To enable reporting, you can create a handler class that implements the
IStateChangeHandler
interface then pass an instance of this to the engine's
setStateChangeHandler()
method. The engine will then call the handler's handleStateChange()
method passing a StateChangeRecord
object as an argument, for every state change that it performs.
The BasicStateChangeHandler
class provides a basic implementation that reports state changes one per line
on stdout. To enable its use:
engine.setStateChangeHandler(new BasicStateChangeHandler());
You can also set the state change handler class by specifying a configuration file as a system property. This is described later.
Setting the state change handler to null
disables reporting of state changes.
There are three thread schemes available:
The thread scheme is set by calling the engine's setThreadScheme()
method and, for thread-per-engine mode,
setThreadCount()
sets the number of processing threads. See the API description for details.
Note that any state method that performs a blocking operation (waiting for user input, for example) will stop the associated thread completely until it becomes unblocked.
Certain engine characteristics, as well as user-defined values for use by the application, can be set in a properties
file and loaded by the Engine class automatically when it is loaded. To do this, specify the location of the file as
the jspasm.configuration
system property. If the file is in the classpath (for example, in a Jar file
containing your application) it can be loaded by specifying the path:
java -Djspasm.configuration=com/mydomain/mypackage/jspasm.properties ...For files external to the application, the location can be identified similarly:
java -Djspasm.configuration=file:configuration/jspasm.properties ...Note that in this case the value is a URL and requires a protocol - use
file:
as shown above when the
properties file is on the machine where the application is running.
The properties file may contain these keys:
jspasm.exception.handler
: Specifies the fully-qualified name of an Exception Handler class. If this key
is not in the properties file the Engine class will create an instance of BasicExceptionHandler
and this
instance will be shared by all Engine instances. If the key is in the properties file and has an empty value, no
exception handler is created and no exceptions will be reported. Otherwise one instance of the specified class will be
created and shared by all Engine instances.
jspasm.statechange.handler
: Specifies the fully-qualified name of a State Change Handler class. If this
key is not in the properties file or the key is in the properties file and has an empty value, no state change handler is
created and state changes will not be reported. Otherwise one instance of the specified class will be created and shared
by all Engine instances.
jspasm.thread.scheme
: Set to "engine"
, "model"
or "entity"
to
specify thread-per-engine, thread-per-model or thread-per-entity modes of operation respectively. The setting becomes the
default for all engine instances created.
jspasm.thread.count
: For the thread-per-engine mode, this sets the default number of processing threads.
The properties file can also be used to make application-specific settings available to user code. The
Properties
object loaded from the file is a static field publicly accessible as Engine.cfgProp
.
The speed of the engine depends primarily on the speed of the CPU and to a lesser degree upon the installed JVM, the platform operating system and even the order that JAR files are specified in the classpath.
As a guide, a benchmark using no-argument events/states and null state action methods shows events being generated and delivered at a rate of approximately 450,000 per second when running on a 2.4GHz Pentium IV under Windows XP using the Sun JRE. The benchmark program is available for download on the JSpasm home page.
The Engine class places no limits on the number of models, entities and events that can exist at any time. As such, it is possible under extreme conditions to create so many entities and events that out-of-memory errors could occur. Developers should be aware of this when developing potentially large systems.
From experience we have found that the following naming guidelines work quite well:
When creating entities it is often necessary to be able to generate entity names that are known to be unique. The
java.rmi.server.UID
and java.rmi.dgc.VMID
classes can be used to generate unique names. See
the documentation of those classes for details.
JSpasm was developed by Pete Ford and is ©Pete Ford & CodeXombie.com. JSpasm is released under the terms of the Lesser GNU Public License (LGPL).
The JSpasm package and the demonstration and benchmark programs mentioned in this document are available for download from the JSpasm homepage. Check the homepage for updates and news about JSpasm.