Selathco 0.91 generated HTML
Predchozi Table of contents ) Dalsi

2. Inside J-Sim Kernel

In this chapter, an explaination of the basic principles on which the J-Sim kernel is based will be presented. It will discuss mainly the way in which processes are switched, how they are suspended and reactivated and the relationship between a simulation and its processes. It is assumed that the reader has already some knowledge of the Java language and concurrent programming.

2.1. Possibilities Offered by Java

The Java programming language provides a set of classes, interfaces, methods and keywords manipulating with threads. Only some of them will be mentioned, those which are particularly interesting for realization of pseudo-parallel execution.

2.1.1. The Thread Class

First of all, the Thread class must be mentioned. An instance of this class or its subclass (1) is able to be executed parallelly with the thread that created and ran it. Every instance of this class has its parallelly executable code stored in the run() method which is empty in the case of Thread. This method should be always overwritten in order for the thread to provide some meaningful concurrently-run services. When an instance of a Thread's subclass is created, it is passive like all ordinary Java objects. It is switched to an active state using its start() method. This causes the code of its run() method to start to be executed parallelly. Inside this method, other methods (of the same object as well of other objects) can be called without breaking up the parallelism. Once the run() method reaches its end, the object becomes passive again without the possibility to be restarted. But it can still provide non-parallel services by methods invoked in the usual way. There is always at least one thread within every Java program: the main() method of the class which was given as a parameter to the Java interpreter. Here is a little example how to create and run a thread which prints out a message:


class MyThread extends Thread
{ public void run()
  { System.out.println("Hello."); }
}

. . .

// In main()
MyThread thread = new MyThread();
thread.start();

2.1.2. Unusable Synchronization Methods

The Thread class provides three very promising methods allowing to manipulate with a thread's execution state: suspend(), resume(), and stop(). The suspend() method temporarily suspends execution of a thread until resume() is invoked on the same thread. The last method interrupts execution of a thread and causes it to exit its run() method. However, all of them have been marked as deprecated since JDK (2) 1.2. There were very serious reasons to do so, mainly concerning security. The reasons can be found in [Lea].

2.1.3. Usable Synchronization Methods

However, Java provides a very efficient way of thread synchronization, although it is not obvious for the first sight. A method (3) of a class can be marked as synchronized (using the synchronized keyword in its header) which will prevent all threads from entering method's body if there is already another thread. A lock is used to keep information whether it is possible to enter the method. If there are more protected methods within a class, they share the same lock. Note that the lock belongs to an object (instance of a class) not to a class. Here is a simple example of two synchronized methods sharing the same lock:


public synchronized void a()
{ /* ... something is happening here ... */ }

public synchronized void b()
{ /* ... and something else here ... */ }

When a thread enters the a() method, the lock is acquired and no other thread can enter this nor the b() method. All threads attempting to do so are temporarily suspended and one of them is resumed when the first thread leaves the a() method and the lock is released.

However, methods or just their parts can be synchronized by another lock than the implicit one (this). The following piece of code protects the methods' code in exactly the same way as the last one, but uses an explicit lock which must have been properly initialized. The lock can be instance of any class.


public void a()
{ synchronized (theLock)
  { /* ... something is happening here ... */ }
}

public void b()
{ synchronized (theLock)
  { /* ... and something else here ... */ }
}

Note that exactly the same method of thread synchronization is used in J-Sim. In order to be able to synchronize their methods with methods of all other processes, processes must share the same lock. This will be described in the following chapters.

There are techniques offered by Java that allow a thread to become temporarily suspended inside a synchronized method (or just a block of code) in order to release the lock and thus allow another thread to enter the synchronized method or block. Method wait(), introduced already in the class Object, provides this service. The suspended thread must be woken up sometimes not to spend all its life in passive state, being not able to do anything. There exist two methods usable for this purpose: notify() and notifyAll(). They don't differ in behavior if there is just one thread suspended. If there is more than one thread suspended, the notify() method wakes up just one of them, it is not specified which one. The notifyAll() method wakes up all of them. However, they cannot start to run parallelly since they wake up inside a synchronized block of code. Only one of them is allowed to run (not specified which one) and all others must wait until the selected thread leaves the synchronized block of code. As soon as it does so, another already woken-up thread is selected and allowed to run and so on. A typical example on how to use these methods (notify() is omitted here but the principle is the same) is as follows:


public void sleepUntilWokenUp()
{
 synchronized (theLock)
 {
  // This piece of code will be executed right after entering
  // the synchronized block. This can take a while, too,
  // if there is already another thread.
  theLock.wait();  // suspended
  // This piece of code will be executed
  // after the suspended thread
  // is woken up and the thread that woke him
  // up in wakeThemUp()
  // leaves the synchronized block.
 }
}


public void wakeThemUp()
{
 synchronized (theLock)
 {
  // This piece of code will be executed right after entering
  // the synchronized block. This can take a while, too,
  // if there is already another thread.
  theLock.notifyAll();  // wake up all suspended threads
  // This piece of code will be executed right after waking other
  // processes up, there is no time delay.
 }
}

Classes having these synchronized methods are not usually threads but passive data containers. They use synchronization to protect their data from being overwritten or read in a bad way by two concurrently running threads trying to change and read the value of a variable in the same moment. Such classes are called monitors.

However, nothing prevents programmers from implementing this mechanism into a class which is a subclass of Thread. But in this case, the programmer must be extremly careful about liveness od such threads. It is obvious that they cannot use their private locks. If a thread passivated itself, it would not be woken up anymore since other threads, trying to wake it up, would use their own private locks (myPrivateLock.notifyAll()). Therefore, the waking-up signal would never reach the suspended thread! Instead, all threads must use the same lock. Then, it is sufficient if at least one thread is in runnable state because it is able to wake up all others by invoking the notifyAll() method on their common lock. Exactly the same principle is used by J-Sim kernel to switch between user processes and the main thread, requesting to do one simulation step.

2.2. Simulation as Container

As it has been mentioned above, in order for user processes to switch properly they need to have one unique lock shared by all of them. Trying to analyze the situation deeper, one will find that there is not running the same number of threads as the user processes. There is always one in addition to them - the main thread, which executes the code written in method main() of the class whose name was given as the first parameter to Java interpreter java (or java.exe). This thread is created and run automatically by JVM. It is usually this thread which calls the step() method of the simulation but does not necessarily need to be. Because it is required that this main thread wait until the step is finished (and all user processes are suspended), it must be synchronized by the same lock as processes are. Actually, it is not the main thread which owns the lock. The lock is owned by the simulation and a reference to it can be obtained by invoking simulation's method getGlobalLock(). In fact, the main thread need not care about this lock at all. When it invokes the step() method of the simulation, it has full access to all its variables, therefore to the lock too.

Simulation as Container
Simulation as Container

The situation is depicted in figure 2.2.. The lock named globalLock is owned exclusively by the simulation object and its processes get just a reference to it. Because every process has its simulation to which it belongs (called myParent) it can get the reference upon creation (in constructor JSimProcess) by invoking myParent.getGlobalLock(). The fact that the reference's name is ourCommonLock does not have any influence. Despite different names in different classes it is still the same object. Now, all user processes and any arbitrary thread invoking the step() method of their simulation can be synchronized by the same lock.

Ignoring the rest of the image for the moment, the process switching using this lock will now be examined.

2.3. Execution of a Step - Switching Techniques

2.3.1. Points of Switching

Before explaining the switching techniques it would be convenient to state where exactly the switching takes place. In class JSimSimulation, it is:

  • in the step() method when the simulation has selected a process to be run in this step - the simulation must passivate the main thread and activate the process.

In class JSimProcess, switching points are:

  • in the getReady() method when a newly created and started process must passivate itself not to execute commands that follow - these commands can be executed only upon a request from the simulation. This cannot be considered as full switching since no thread has to be activated because the simulation (the thread calling the step() function, respectively) has not been suspended;

  • in the passivate() method when the currently running process (the only running process) must passivate itself and activate the thread which invoked the step() method of the simulation. This is the full switching since there is only one thread running before and one after the switching point and they are not identical;

  • in the hold() method when the currently running process (the only running process) must passivate itself and activate the thread which invoked the step() method of the simulation. The technique of switching does not differ at all from the last case, the hold() method just creates a new event in simulation's calendar. Again, this is the full switching.

2.3.2. Commands Used to Switch

The deterministic switching (4) is possible thanks to the following factors:

  • A common lock, owned by the simulation, exists. This lock is called globalLock in JSimSimulation and ourCommonLock in JSimProcess.

  • A calendar of events, owned by the simulation, exists and processes have right to add new events into the calendar. This calendar is called calendar.

  • Every process is identified by a unique number within its simulation (myNumber) and the simulation has a variable determining the number of currently running process: runningProcessNum. There exists a special constant NOBODY saying that no process should run, only the main thread. Processes cannot change the value of this variable; this operation is reserved exclusively for the simulation. However, they can use simulation's switchToNobody() method which sets runningProcessNum to NOBODY. This means that a process cannot switch to another process, only to the main thread. This guarantees that during one call to step() there will be executed really one step only, not more.

Let's have a look now how the switching is implemented in Java. First, switching from the main thread to a process (a part of JSimSimulation.step()):


runningProcessNum = calendar.getFirstProcessNum();
time = calendar.getFirstProcessTime();

/* Now sending the wake-up signal to all threads in the system */
globalLock.notifyAll();

/* Now waiting until "hold()" or "passivate()" is called. */
while (runningProcessNum != JSimCalendar.NOBODY)
{
 try
 {
  globalLock.wait();
 }
 catch (InterruptedException e)
 {
  /* Just ignoring this exception -- nobody
     should iterrupt the main thread */
 }
} // while

First, the runningProcessNum variable is set to the value which is taken from the top of simulation's calendar. This process is intended to be run in next step. Then, the time of the simulation is set to the value of the first event in the calendar. After that, all processes belonging to the simulation are woken up by a signal sent by notifyAll(). When they receive the signal, they check whether they are intended to run (they compare their number, myNumber, with the result of the call to myParent.getRunningProcessNum()). If a process finds that it is its turn, it just continues. All others suspend themselves again.

After notifying all processes, the main thread passivates itself, using globalLock.wait(). Note that the notification must be done before passivation - a passivated process cannot notify any other process since it is stuck at one point. The wait() method may throw out an InterruptedException if the thread is interrupted by another thread in suspended state. Here, the case that such an exception is thrown out is simply ignored because it is not expected here. When the main thread is woken up, it checks whether it is intended to run (runningProcessNum is set to NOBODY) and continues running or passivates itself again. Because processes may only switch back to the main thread, this condition will always be false and the cycle will never run twice. However, it is a widely accepted convention to wrap every wait() into a while cycle. It is completely harmless and illustrates well the programmer's intentions.

Now, a look at how processes switch back to the main thread will be given. The method of switching in hold() is identical to the method in passivate(), thus it will be explained at once:


synchronized(ourCommonLock)
{
 /* Wake up main() which is waiting now in step() */
 myParent.switchToNobody();
 ourCommonLock.notifyAll();

 /* Going to sleep */
 while (myParent.getRunningProcessNum() != getProcessNumber())
 {
  try
  {
   setProcessState(STATE_PASSIVE);
   noOfCalendarEntries--;
   ourCommonLock.wait();
  }
  catch (InterruptedException e)
  {
   shouldTerminate = true;
   requestsToTerminate++;
   break;
  }
 } // while
 setProcessState(STATE_ACTIVE);

 /* If we have to die this exception is propagated till run() */
 if (shouldTerminate)
  throw new JSimProcessDeath();
} // synchronized

This is a more complicated example of switching since it must be able to hanle process interruptions. Processes are interrupted by the simulation when the user wants to terminate the program and calls the shutdown() method. The way a process can be terminated is subject of the next chapter.

First, the process tells the simulation that it wants to passivate itself and the main thread should run - myParent.switchToNobody(). Then, the waking-up signal is sent to all threads passivated with the same lock. After checking the number of process intended to run, they all passivate themselves again, except for the main thread which invoked the step() method of the simulation. Then, the currently running thread passivates itself (ourCommonLock.wait()). When it is woken up, in the next simulation step when the simulation needs to switch to the selected thread, it compares the number of the process selected to run (which has changed since this process' passivation) to its own number and decides whether to run or passivate itself again.

If a process is interrupted by the mechanism mentioned above and discussed in the next chapter, it sets its interruption flag shouldTerminate to true and throws out a new instance of JSimProcessDeath exception.

2.3.3. An Example of One Simulation Step

To better illustrate what has been said about process switching, figure 2.3.3. on page 2 gives an explanation.

One Simulation Step Illustrated
An Example of One Simulation Step

When the main thread asks the simulation to execute a step, there are four processes already present within the simulation. They are all suspended in the passivate() or hold() methods. Some of them could be suspended in the getReady() method, too, if they have not run yet. Because J-Sim allows processes to be created dynamically when the simulation is already running, at the beginning of every step the newly added processes must be started. This task is performed by startNewProcesses(). In this example, one process was added to the simulation in the last step and must be started now. As soon as it is started, it passivates itself in the getReady() method.

After new processes are started, the simulation looks into the calendar of events (using calendar.getFirstProcessNumber()) and finds that it is process number 2 which should be run in this step. It stores this number into the variable runningProcessNum and sends a wake-up signal to all suspended processes. Immediately after that, the currently running thread is passivated.

When processes (including the newly created one) receive the signal and are woken up, they must decide whether to run or to passivate themselves. Most of them choose the second possibility. Only one of them, the process having number 2, decides to continue. It continues running a part of its life as the only thread within the simulation until it calls the hold() or passivate() method.

When it does so, runningProcessNum is set to NOBODY by calling myParent.switchToNobody(). Then, all threads are woken up again by notifyAll(), including the main thread. Immediately after notifying them, the process having number 2 passivates itself. All other threads, except of the main one, passivate themselves again because they find that they are not intended to run. The main thread removes just executed event from the calendar and exits the step() function. One simulation step has just completed.

2.4. Life of a Process

Every Java thread has its own run() method that the user can (and should) overwrite in his subclasses to give his threads a specific behavior. Normally, this method does nothing and a thread which has just been started immediately terminates. Because J-Sim's class JSimProcess is a subclass of the Thread class, the run() method can be overwritten in a convenient way to fulfil the user's needs. However, this would be quite complicated for him because every J-Sim process must take care of the following things:

  • At the very beginning of the run() method, every process must passivate itself. This is due to the fact that a part of a process' life can be executed only upon its simulation's request (activation by a signal from the step() method), not in a wilful manner as Java threads usually run.

  • At the end of the run() method, the process must let the simulation (myParent) know that it is dying and cannot be run anymore.

  • Between these two points, there must be a mechanism allowing the thread to finish in the usual way when it is suspended in its hold() or passivate() method and the simulation is shutting down - its shutdown() method is called.

  • There must be a mechanism preventing the thread from collapsing when the user commits an illegal operation which would normally destroy the thread.

Since these requirements are always present and it is quite complicated to fulfil them from the user's point of view, the following strategy was applied:

  • The run() method is marked as final which means that the user cannot overwrite it in his subclasses. This method invokes getReady() at the beginning which passivates the process. At the end, the finish() method is invoked to let the simulation know that this process is dying. In between, the life() method is called.

  • In JSimProcess, the life() method is empty. Because this method is not marked as final, the user can (and should) overwrite it. This method represents the life of the user's process.

2.4.1. Two Kinds of Death

Taking a deep look at the possible strategies of processes' lifes, one will find that there are several of them:

  • Finite processes which finish their life() method by reaching its end. These processes usually contain a few life parts divided by the hold() function. Their death can be called natural because they decide to die voluntarily.

  • Infinite processes which contain a neverending loop inside their life() method. Such processes never finish voluntarily and the simulation must kill them when its shutdown() method is called. Because the shutdown() method is invoked outside of any simulation step, typically at the end of the main program, all such processes are suspended in the hold() or passivate() method and must be killed.

  • Finite processes which have not had a chance to finish their lives because too few simulation steps has been executed before the shutdown() method is called. These processes are suspended in the hold() or passivate() method and must be killed too.

  • Finite or infinite processes which have not been run yet at all. These processes are suspended in the getReady() method and must be killed too.

The second way of process termination - killing - requires a special mechanism which is discussed in the next chapter.

2.4.2. Killing a Process

As already known, the technique of process killing is used when a process is suspended in hold(), passivate() or getReady() and its simulation tries to destroy it. As mentioned in chapter 2.1.3., every suspension (a call to aLock.wait()) must be wrapped into a try block followed by an exception handler catching all instances of InterruptedException.

Since it is not recommended to use deprecated methods such as stop(), the only possibility how the simulation can interrupt a process is to invoke the interrupt() method: aProcess.interrupt(). Because the simulation knows about all processes belonging to it, it can interrupt all of them.

When a process is interrupted while being suspended, an instance of class InterruptedException is thrown out from the body of the wait() method and is caught in the exception handler which follows the mandatory try block. It is an easy job to set a flag saying that the process should die to true. The corresponding piece of code follows:


try
{
 setProcessState(STATE_PASSIVE);
 ourCommonLock.wait();
}
catch (InterruptedException e)
{
 shouldTerminate = true;
}

But another problem appears: The methods hold() and passivate() cannot be just exited. These methods are called from the life() method which always contains user's code. And he has no interest in taking care about exiting the life() method when it is necessary, i.e. in testing the flag of interruption. Therefore, a similar principle like in Java Core API is used: an exception is thrown out, which is not caught in the life() method but is caught in the run() method. Although this principle has been deprecated in Java Core API because it is possible to catch the exception and force the dying thread to stay alive, it suits its purpose very well. For the detailed reasons of deprecation, see [Lea], page 175. Although getReady() is not invoked from life(), it uses the same principle to unify the methods of process killing used. Here is the corresponding piece of code:


if (shouldTerminate)
 throw new JSimProcessDeath();

Since JSimProcessDeath is a subclass of RuntimeException, it need not be enumerated in the list of exceptions thrown out from a method. Therefore, the headers of methods that this concerns look as follows:


private final void getReady()
protected void life()
protected final void passivate()
     throws JSimSecurityException
protected final void hold(double deltaT)
     throws JSimSecurityException

Note that JSimSecurityException has nothing to do with the principle of process killing, it is just a way how to tell the user that he tries to commit an operation which is not allowed.

A little summary of JSimProcessDeath spreading follows. It contains a list of all possible paths through which this exception can spread:

  • getReady() --> exception handler in run()

  • hold() --> life() --> exception handler in run()

  • passivate() --> life() --> exception handler in run()

2.4.3. Life of a Process Illustrated

The run() method, complexly described in previous chapters, is depicted in figure 2.4.3. on page 2.

Life of a Process
Life of a Process

The thick black arrow leading from getReady() to life() represents the moment when the process is selected for the first time by the simulation to run. The second thick black line leading from life() to finish() represents the process' natural death, i.e. the moment when the process reaches the end of life() by its own decision.

As it can be seen, there are three points where the process can be killed by its simulation: in getReady(), hold() and passivate(). It does not matter that the hold() and passivate() methods are not invoked directly from run() because the JSimProcessDeath exception is able to propagate through the life() method since the user does not care about it. The exception is caught in the handler, placed between the life() and finish() methods. Then, the execution continues by invoking finish(), as usual.

2.4.4. Possible Danger of Catching JSimProcessDeath

The solution used by J-Sim to kill processes contains one hidden danger. Nothing can prevent the user from catching all JSimProcessDeath exceptions in the body of his life() method. He can do that if he uses at least one hold() or passivate() method in his life() because this exception is thrown out by them. In that case, the user is fully responsible for terminating the process and J-Sim has no other possibilities to do it. The following list contains possible solutions how to treat JSimProcessDeath exception in the life() method, beginning with the best one and ending with the worst one:

  1. Ignore the existence of JSimProcessDeath completely. Neither write any exception handler nor add this exception to the list of exceptions thrown out by this method. This is the recommended solution.

  2. Write an exception handler catching JSimProcessDeath but re-throw it from there. Again, this exception need not be in the list of exceptions thrown out by life(). An example follows:
    
      . . .
     } // try
     catch (JSimProcessDeath death)
     {
      // Some useful action(s) here...
      throw death; // This is important!
     } // catch
    } // life()
    
    

  3. Write an exception handler catching JSimProcessDeath, not re-throw it, but store the flag of need to terminate to a variable. At least once per cycle that the life() method usually contains check this variable and exit life() immediately. An example follows:
    
     while (!terminateLater)
     {
      try
      {
       // Process' life, including hold()s and passivates()s
      } // try
      catch (JSimProcessDeath death)
      {
       // Some useful action(s) here...
       terminateLater = true; // Setting the flag...
      } // catch
     } // while
    } // life()
    
    
    However, if there are more than one hold() or passivate() methods called from inside the cycle, this solution will cause a deadlock because no process will wake up this suspended thread anymore.

  4. Write an exception handler catching JSimProcessDeath and ignore that this exception was caught. This solution will inevitably lead to deadlock if the process invokes at least one hold() or passivate() after catching the exception. The reasons are the same as in previous point.

2.4.5. Prevention from Collapses

During execution of the process' life, number of runtime exceptions (5) can be thrown, for example NumberFormatException if the user tries to convert a badly formatted string to number.

These exceptions would normally propagate from life() through run() to JVM which would destroy the affected thread. Because this thread is the only running one at the moment, its destruction would inevitably lead to deadlock. All other threads, including the main thread, would stay suspended and the only visible result would be a "hang-up".

To avoid this situation, another exception handler must be inserted into the run() method, which will catch all exceptions caused by the user's programming mistakes. After catching the exception, the process continues as a normally terminated process (see natural death in chapter 2.4.1.), i.e. by calling the finish() method.

2.5. A Deep View of JSimSimulation

In this chapter, a presentation of the JSimSimulation class in deeper detail with focus on some important fields and methods it provides for its processes and for the user will be given. Not all of them will be mentioned, only those of sufficient importance.

JSimSimulation is one of J-Sim's fundamental classes. It is inherited from Object and thus has no special pre-programmed behavior or services to offer. Only this class is able and allowed to execute the simulation in the step-by-step manner mentioned in chapter 1.1.. An instance of this class must be present in every J-Sim simulation program. If it is not, it is not possible to create any process and queue (see chapters 2.6. and 3.). It is not necessary to create a new subclass of JSimSimulation. It is a ready-to-use class whose instances are fully functional.

Here is a list of principal tasks that every simulation object is given:

  • The simulation object has a list of processes which must be updated whenever a new process is created or a process dies.

  • The simulation object has a calendar of events (instance of JSimCalendar) which must be updated whenever a process is activated (activate()), temporarily suspended (hold()), or cancelled (cancel()).

  • The simulation object must keep track of simulation time. The initial simulation time is 0 and it changes discretely with every step executed.

  • The simulation object must know which of its processes is currently running, if a simulation step is being executed.

  • The simulation object is responsible for switching between console mode and graphics mode of output - method runGUI() (6) .

  • To the user, the simulation object offers the possibility to execute one simulation step. The step() method provides this service.

2.5.1. Information about Processes

Every instance of JSimSimulation is equipped with a queue of JSimProcessInfo elements where it stores information about processes present in the simulation. The name of the queue is infoQueue. Whenever a new process is added to the simulation, a new JSimProcessInfo object is created and inserted to the queue. JSimSimulation class provides a method called addProcess(), taking one parameter - a reference to the process to be added. This method is called implicitly from the constructor of JSimProcess, therefore the user need not take care about that.

There are two methods deleting processes from the queue: deleteProcess() and deleteAllProcesses(). The difference between them is that deleteProcess() deletes just one process from the queue, whose number is specified as parameter of the method, while deleteAllProcesses() destroys the whole queue, sending all processes in the queue an interrupt signal. Why is the signal sent in deleteAllProcesses() but not in deleteProcess()? The answer is easy: deleteProcess() is called by the process itself from its finish() method when it is dying by a natural death while deleteAllProcesses() is called from simulation's shutdown() method when there are some threads suspended. It is thus necessary to kill them by interrupting their suspension. What follows after the interrupt signal is received is described in chapter 2.4.2..

2.5.2. The Calendar

The calendar (a variable called calendar) is an instance of the JSimCalendar class. It is nothing more than a queue of JSimCalendarItem elements. Every such element holds information about one event: time that the event is scheduled for - time - and number of the process which will be run at that time - processNum. The calendar provides some important methods that the simulation uses:

  1. addEntry() adds a new element to the queue. Absolute time of the newly added event and a process' number must be specified.

  2. deleteEntries() deletes either the first or all events from the calendar concerning a process. The process' number and the choice "first/all" must be specified.

  3. getFirstProcessNum() returns the number of the process which is pointed to by the first element in the queue.

  4. getFirstProcessTime() returns the time of the first event in the calendar. Process obtained in point 2.5.2. will be run at this time.

  5. jump() deletes the first event from the calendar and sets the beginning of the queue to the following element. This method is invoked from the simuation's step() function when a step is completed.

Since processes do not know anything about the existence of the calendar they cannot use the methods mentioned above. Instead, JSimSimulation offers them methods called addEntryToCalendar() and deleteEntriesInCalendar() to insert and delete events. These functions are marked as protected and therefore can be invoked only from JSimProcess, not from its subclasses not being in the package cz.zcu.fav.kiv.jsim. They are used in activate(), cancel(), and hold() when processes need to create or delete an event.

2.5.3. The Simulation Time

The simulation time, stored in variable time, is a floating-point number which changes just before a simulation step starts to be executed. As it can be seen in figure 1.2. on page 1, the value of the change is not constant and cannot be predicted in any way unless the time of the next event in the calendar is known.

There is only one place in the code of JSimSimulation where the simulation time is changed: it is in the step() method when the new time is taken from the calendar and before the main thread switches to a selected process:


time = calendar.getFirstProcessTime();

Although it cannot be set by anybody else than the simulation, it can be read by everybody: processes, user - outside the step() function, etc. Processes can obtain the simulation time by calling myParent.getCurrentSimulationTime(), the user can obtain it by calling x.getCurrentsimulationTime() where x is a simulation object created in the main program.

2.5.4. Switching to Graphics Mode and Back

Every J-Sim simulation is able to run in two modes: in console mode and graphics mode. Implicitly, it is the console mode that the simulation runs in and the user must use a method called runGUI() to switch to graphics mode. Because it is not as trivial of an operation as it seems to be at the first sight, it must be explained here.

As stated in chapter 2.2., every simulation is able to create its own simulation window which is an instance of the JSimGUIMainWindow class. The internals will be discussed in chapter 2.7., while here the explaination of the relationship between the simulation and its window will be given.

To create a new window (an instance of JSimGUIMainWindow) and open it using its show() method, one will find that the window is visible and properly functionning but the show() method does not suspend the calling thread for the time the window is open. Instead, four (7) new threads are created to handle the user input (mouse, keyboard) and perform graphical operations. The calling thread continues running without waiting for the window's closure. This is the fundamental problem that must be dealt with.

The solution is obvious if figure 2.5.4. on page 2 is examined. It is exactly the same figure that was presented on page 2.

The Relationship between a Simulation and It's Main Window
Synchronizing the Simulation with the Main Window

If the main thread after window's creation is suspended until the time the window is closed, the desired behavior will be obtained. To be able to do that, a new lock must be introduced, shared by the simulation object and the window. This lock is called graphicLock both in JSimSimulation and in JSimGUIMainWindow. Again, the lock is physically created only once (in the simulation's constructor) and the simulation window has only a reference to it. The mechanism of switching is less complicated than the mechanism of process switching but it will be explained anyway.

When the main thread creates the window and opens it, it must passivate itself until the main window sends it a wake-up signal. Again, the wait() method of the shared lock is used:


synchronized (graphicLock)
{
 mainWindow = new JSimGUIMainWindow(this);
 while (windowOpen)
 {
  try
  {
   graphicLock.wait();
  }
  catch (InterruptedException e)
  {
   windowOpen = false; // We must get out
  }
 } // while
} // synchronized

When the main thread is woken up, it checks the value of windowOpen. If it is false, it exits the while cycle and consequently exits the runGUI() method. At this point, windowOpen should never be true again because the only sender of the wake-up signal is the main window which always sets this variable to false before the signal is sent. The following is the corresponding piece of code in JSimGUIMainWindow's event handler:

synchronized (graphicLock)
{
 myParent.windowIsClosing();
 graphicLock.notifyAll();
 hide();
} // synchronized

Since the window itself cannot set the value windowOpen to false because it is a private variable, it must use the windowIsClosing() method of the simulation - myParent. Then, it sends the wake-up signal to all threads suspended at the lock. However, there is only one such a thread - the main one. After that, the window is hidden.

Once a graphics window using AWT (8) services is created, Java creates some auxiliary threads, as mentioned above. These threads are not destroyed when the window is closed, they cannot be destroyed by sending them the interrupt signal - aThread.interrupt() either, because this signal can be (and probably is) ignored by them. Since the Thread.destroy() method has not been implemented (9) there is no other way to prevent the program from hanging up unless the System.exit() method is used to exit back to operating system. Because it is done in the shutdown() method which is always called at the end of the program, neither the simulation nor the user are affected.

2.5.5. The step() Method in Detail

For the user, the step() method is the only really useful simulation's method. Although the principle of process switching, managed by this method, has been already discussed in chapter 2.3.2., a complex and detailed look at the method, explaining all possibilities that can occur during its execution, will be given.

First of all, the simulation must start all processes which were added to the simulation during last step. Therefore, startNewProcesses() is invoked at the very beginning of the method. After that, the execution differs according to the state in which the simulation is. The state is stored in a variable called simulationState. Every simulation can be in one of the three following states:

  • Not-started-yet state. The simulation is in this state if no step has been executed yet. Its respective constant is SIMULATION_NOT_STARTED.

  • In-progress state. The simulation is in this state if at least one step has been executed and the simulation has not terminated yet. Its respective constant is SIMULATION_IN_PROGRESS.

  • Terminated state. The simulation gets to this state if there are either no processes or no calendar events and the user tries to do a step. Its respective constant is SIMULATION_TERMINATED.

If the simulation has not been started yet, the step() method checks whether there is at least one process within the simulation. If there is not, the simulation sets the state to terminated and does not continue anymore, otherwise it sets the state to running (in progress) and continues normally.

If the simulation has already terminated, the step() method does not do anything and simply exits.

In the usual case (simulationState is equal to SIMULATION_IN_PROGRESS), the step() method takes from the calendar the first process' number and time and store these values to runningProcessNum and time. Then it checks whether the selected process really exists (it must be found in the queue of processes inserted into the simulation - infoQueue) and has not finished yet - isAlive() must return true. When one of the tests fails, it is necessary to repeat the selection. If there are no more events in the calendar or infoQueue is null (there are no processes), the simulation is terminated.

Finally, when a process is selected, the switching mechanism, described in chapter 2.3.2., is used, the selected process is woken up and running and the main thread is suspended. When the process switches back to the main thread, the event just executed is removed from the calendar by calling calendar.jump().

The method returns a boolean value saying whether it is possible to execute another step or not.

2.6. A Deep View of JSimProcess

Like JSimSimulation, JSimProcess is one of J-Sim's fundamental classes. It is a subclass of the Thread class and therefore inherits all its possibilities, mainly the possibility to be executed concurrently to other threads. New subclasses of JSimProcess should be always created since the life() method, representing the life of the process, is initially empty in JSimProcess. The only necessary changes in such a subclass are:

  • new constructor, since JSimProcess' constructor cannot be invoked automatically because it has two parameters;

  • new implementation of the life() method.

2.6.1. States of a Process

During its life, every process can be in one of four different states:

  • new - A process is in this state typically for a very short time between it is created in one simulation step (or before any simulation step) and it is started in the following simulation step, when the simulation invokes startNewProcesses(). Once a process is started and enters its getReady() method where it is suspended, the process becomes passive and cannot become new anymore. The corresponding constant is STATE_NEW.

  • passive - A process is in this state for most of its life. Typically, it switches to this state whenever hold() or passivate() are invoked and it stays in this state until the process is woken up by the simulation, exits one of these methods and starts to execute a part of its life. When it invokes passivate() or hold() again, it switches back to the passive state. The corresponding constant is STATE_PASSIVE.

  • active - A process switches to this state when it is woken up by the simulation and starts to execute a part of its life. It can switch back to the passive state by invoking hold() or passivate() and to the terminated state by reaching the end of its life() method. Only one of all processes present in the simulation can be in the active state while all others are new, passive or terminated. The corresponding constant is STATE_ACTIVE.

  • terminated - A process switches to this state automatically when it reaches the end of its life() method or is killed by its simulation. Once it gets to this state, it cannot switch to any other state or be restarted. The corresponding constant is STATE_TERMINATED.

Unlike in C-Sim, there is not any state called scheduled. The reasons for this will be discussed in chapter 2.6.2..

2.6.2. The Concept of Over-scheduling

In C-Sim, every process can have at most one event in the calendar. In J-Sim, a different approach was chosen. A process can have unlimited number of events in the calendar. This implies the possibility to schedule an already scheduled process. All already present events belonging to the process are kept in the calendar and a new one is inserted there. There are three possibilities how a new event can be added to the calendar:

  • Before the simulation is started or between successive simulation steps, a process can be scheduled if its activate() method is invoked:
    
    aProcess.activate(5.67);
    
    
    However, the time specified as parameter must be at least equal to or greater than the current simulation time.

  • A process can be scheduled by another process in its life() method. The scheduling process must have a reference to the process being scheduled in order to be able to invoke its activate() method.

  • A process can schedule itself during its life by invoking its hold() method. This will suspend it for a certain time (the only parameter of the method) and the process will be woken up by the simulation after that period. The process does not need any reference because it calls its own method, inherited from JSimProcess.
    
    hold(0.123);
    
    

Because these methods can be combined, there can be more than one event in the calendar, belonging to a process. It is guaranteed that the process will be woken up at all desired times. However, it is not guaranteed that the process will be passivated for the exact time given as parameter to the hold() method. Another process can activate the process (before or after the process calls hold()) at a time lying in the "passive" interval and the process must be woken up at that time and therefore, must exit the hold() method. In fact, this allows us to "clone" process' life because this method doubles the number of process' events in the calendar.

Therefore, over-scheduling can be considered as extremely useful when the user needs to add process scheduling times from outside but also very dangerous if the user depends on the length of time gaps between two successive steps of process' life.

Since there can be any non-negative number of events belonging to a process, there is no need to register whether the process is scheduled or not. In fact, a process can be scheduled (or not scheduled) in any of the four previous states mentioned in chapter 2.6.1.:

  • In the state called new, a process can be scheduled if another process created it and "activated" (aProcess.activate(time)) in the same simulation step and the step has not finished yet or this happened outside any simulation step (typically before the simulation starts).

  • In the state called passive, a process can be scheduled if another process has activated it (aProcess.activate(time)) and its activation time has not come yet.

  • In the state called active, a process can be scheduled if there are at least two of its events it the calendar: the currently running one and another one. This is the situation of over-scheduling. One of the events could be inserted by the process itself (hold(time)), and the rest of them by other processes (aProcess.activate(time)).

  • Even when a process is terminated, it can be scheduled by other processes. After a process terminates, its code cannot be executed, however the object of the process still exists. When the simulation tries to perform an action on a terminated process, it finds that the process cannot run anymore and ignores the corresponding event.

2.6.3. Five Principal Methods

There are five principal methods provided by the JSimProcess class for the user: life(), activate(), cancel(), hold() and passivate(). The first one is intended to be overwritten in user's subclasses, the four others are marked as final and therefore are unchangeable.

The life() method represents life of a process. The user can put any possible code in it, also code creating new object, including new processes. Initially, this method is empty and it has no sense not to overwrite it in user's subclasses. This method is called automatically from run() and should never be called explicitely by user. To avoid this, the method is marked as protected and thus can be called only from methods of the same class (JSimProcess) or of the same package (cz.zcu.fav.kiv.jsim). The life of a process can be divided into several parts, using methods hold() and passivate(). The method's header is:


protected void life()

The activate() method inserts a new event into simulation's calendar. The event belongs to the process whose method is invoked, not to the process which invokes it during its life. The method takes one parameter: absolute time of activation. The method is marked as public and thus can be called by any process or even the main thread. The method's header is:

public final void activate(double when)

The cancel() method deletes all process' events from the calendar. If the process is passive, it will not be woken up anymore unless activated again by another process. A process can invoke this method on itself as well as on any other process - the method is marked as public. The method's header is:

public final void cancel()

The hold() method temporarily suspends the calling process and wakes it up after the time specified as method's parameter. In fact, it adds a new event to the calendar and then passivates the process. A process can invoke this method on itself only, not on another process. If it tries to do so, a JSimSecurityException is thrown out. The method's header is:

protected final void hold(double deltaT)
     throws JSimSecurityException

The passivate() method suspends the calling process without creating a new event for it. If the process is not over-scheduled now or woken up later by another thread, it will stay passive forever. A process can invoke this method on itself only, not on another process. If it tries to do so, a JSimSecurityException is thrown out. The method's header is:

protected final void passivate()
     throws JSimSecurityException

2.7. A Deep View of JSimGUIMainWindow

The JSimGUIMainWindow class provides a user-friendly method how to run a simulation. Instead of executing steps in the way prescribed in the main program, the user can interactively decide how to proceed with the simulation. The window provides the following possibilities:

  • to execute one simulation step;

  • to execute a given number of steps;

  • to execute an unspecified number of steps, limited by a time value. It will execute as many steps as necessary to reach (or overpass) the time limit;

  • to close the window and continue to execute the simulation in console mode.

In order to handle the user's input (mouse and keyboard input) properly, it creates and registers two event listeners - instances of JSimGUIMainWindowWindowAdapter (for events generated by the window's frame, such as the window's closure) and JSimGUIMainWindowActionAdapter (for events generated inside the window, such as button presses). To describe the model of event creation, propagation and handling, used in AWT (10) is beyond possibilities of this paper. A lot of information on this subject can be found for example at [Java]. The event listeners are the window's inner classes (11) and thus have unlimited access to all of the window's fields and methods. In fact, they do not do anything else except transformation of events, sent by AWT, to method invoking. Here is a list of the events and their corresponding methods, with a brief description:

  • When the button "Run one step" is pressed, actionRunOneStep() is called. This method simply executes one simulation step and then exits. The execution is possible due to the fact that the window has a reference to the simulation it belongs to (myParent) and therefore calls myParent.step(). Look back at figure 2.2. at page 2.

  • When the button "Run N steps" is pressed, actionRunNumberOfSteps() is called. This method reads a number inserted at input line fieldNoOfSteps and executes the same number of steps. If the simulation is terminated before the required number of steps is reached, the method does not proceed.

  • When the button "Run until time limit" is pressed, actionRunUntilTimeLimit() is called. This methods reads the time limit inserted at input line fieldUntil and executes as many steps as necessary until the simulation time is greater than or equal to the limit. If the simulation is terminated before the time limit is reached, the method does not proceed. The window can obtain current time by invoking myParent.getCurrentTime().

  • When the button "Quit" is presed or the window is about to be closed, actionQuit() is called. This method does not destroy the window, it only hides it and sends a wake-up signal to the main thread which is being suspended in the runGUI() method. Before doing so, it must inform the simulation that the window is being closed: myParent.windowIsClosing() is called. The principle of switching from the console mode to the graphics mode was fully described in chapter 2.5.4.. As recalled from the previous chapter, the simulation and the window share a lock called graphicLock in order to be able to do the switching.

The main window has a text area covering most its surface. When the simulation runs in graphics mode, the output generated by JSimProcess.message() and JSimProcess.error() methods is redirected to this area. In the console mode, these methods print to System.out and System.err, respectively.


  • (1) Subclass is a class inherited from the main class using the extends keyword.
  • (2) Java Development Kit
  • (3) Here, according to standard OOP terminology, "method" stands for a function of a class, not a method of synchronization.
  • (4) Such a kind of switching where it is determined in advance which process will run as next.
  • (5) Runtime exceptions are instances of RuntimeException or its subclasses and are not usually caught. They can raise at almost any point of a Java program.
  • (6) GUI stands for Graphical User Interface.
  • (7) Using JDK 1.2.1 under Solaris 8, maybe the number can change.
  • (8) Abstract Window Toolkit, a library for GUI programs provided by Java
  • (9) And will probably never be, according to [Java].
  • (10) Abstract Window Toolkit
  • (11) More information can be found in [Her], chapter 15.

Predchozi
Converted by Selathco 0.91 on 01.09.2001 18:44
Dalsi