9.5 An Improved Gas Station Simulation

 < Day Day Up > 



9.5 An Improved Gas Station Simulation

The gas station simulation that was written in Chapter 3 was limited in that the pump the car would wait on was assigned when the car was created. This was because the pump was modeled as a monitor object. The car had to wait for a pump object to move to the FREE state in the monitor before being able to move to the pump. Because a thread cannot be waiting on two monitors at the same time, the gas pump that was used had to be assigned before the car could begin waiting. If multiple pumps were to be used, the car would have to decide which pump to wait on before beginning to wait. In effect, a multiple pump solution involved lines at each pump with cars waiting for a specific pump. It was not possible to create a single queue of cars and allow them to go to the next available pump.

One other problem with using simple monitors is that the Java wait method is unfair, so it was not even possible to ensure that cars would be serviced in the order in which they arrived. This was graphically illustrated by Program8.7, where the animation showed cars moving to the pump before others that were waiting longer.

This section looks at how to simulate the gas station where all cars are in a single queue waiting for the next available pump. Because of the special problem of matching m pumps to n cars, a simple monitor solution, even with notification objects, just is not powerful enough to implement this simulation. Instead, the Java Event Model can be used with another object, known as an adapter, to create a solution.

The gas station simulation provided here still includes the pump, which has methods that are called in the same order as the methods in Program 3.2; however, these methods are not synchronized. Instead, when the pump is free it will pass a reference to itself to a listener on the pump. In this way, a copy of the pump object will be passed (eventually, as will be shown) to a car object. Because only one car object has a copy of the pump object at a time, the methods of the pump do not need to be synchronized.

If the gas station had only one pump or if a car could be forced to choose a pump when entering the gas station, the car objects could implement a pump listener directly; however, because we have many pumps and one queue of cars, an intermediary object, an instance of the class PumpManager, is used in the design. The purpose of the pump manager object is twofold. First, the pump manager is the pump listener and listens to many pumps. It this way, it de-multiplexes the pumps which effectively allows the cars to wait on more than one pump. The second purpose of the pump manager is to implement a notification object to process the cars in a first-in/first-out order. An object that sits between other objects to manage the interactions between those objects is called an adapter, and in this design the pump manager is an adapter between the pumps and the cars.

A graphic that shows how the gas station simulation works is shown in Exhibits 7 through 9. The overall program works in three steps. In the first step, shown in Exhibit 7, all the pumps notify the pump manager that they are free, and as part of that notification event pass a reference to the pump itself to the pump manager. When the pump manager receives the event from the pump, it places the pump in a vector of free pumps. It then sees if any cars are currently waiting on a pump. If one is, then the pump manager uses its notification object to let it know that a pump is now free for its use.

Exhibit 7: Pump Notifies Pump Manager It Is Free

start example

click to expand

end example

Exhibit 8: Pump Manager Gives Pump to a Waiting Car

start example

click to expand

end example

Exhibit 9: Car Uses Pump and Pump Notifies the Pump Manager When It Is Free Again

start example

click to expand

end example

In the second step, shown in Exhibit 8, the car arrives and asks the pump manager, via a call to get Pump, for a pump to use. If one is available, the pump manager gives the pump object to the car; otherwise, the car is put in a wait queue using a notification object, and the car will be notified later when a pump becomes available via a message from the pump to the pump manager.

The final step is that the car now calls the methods on the pump object to complete the simulation. When the car completes, it calls the pump's leave method, which generates an event to the pump manager, freeing this pump and notifying the pump manager that it can now assign this pump to another car.

The code for implementing this gas station simulation is given in Program9.4 (Exhibits 10 through 15). Because the pump events are implemented using the Java Event Model, it was necessary to implement a listener interface (Exhibit 10 [Program9.4a]) and an event object (Exhibit 11 [Program9.4b]). The Pump, Car, and PumpManager classes are implemented in Exhibits 12 to 14 (Program9.4c, Program9.4d, and Program9.4e). Finally, the overall controlling program, the GasStation class, is implemented in Exhibit 15 (Program9.4f).

Exhibit 10: Program9.4a: PumpListener Class

start example

 import java.util.EventListener; public interface PumpListener extends EventListener {   public void pumpReady(PumpEvent e) ; } 

end example

Exhibit 11: Program9.4b: PumpEvent Class

start example

 import java.util.EventObject; public class PumpEvent extends java.util.EventObject {   private Pump pump;   public PumpEvent(Object source, Pump pump) {     super(source);     this.pump = pump;   }   Pump getPump() {     return pump;   } } 

end example

It should be noted that the only objects in this simulation containing threads are the cars. So, when the car calls the leave method, and the pump generates an event to the pump manager, the event in the pump manager runs in the car thread. This involves the thread going across two different passive objects. A much safer design would isolate this deep of a notification in a separate thread to prevent unforeseen delays or even possible deadlocks. This would be a particular problem if the pump manager could call back to the car that started the event; if the methods in the car were synchronized, then deadlock could occur. For this simulation this is not an issue, as the Car object is complete as soon as the event occurs, and in no methods could the car be called from the pump manager or pump; however, this level of indirection on a thread is generally not advisable. The chat program in Chapter 12 has such a deep calling structure and the possibility of just such a deadlock occurring.

We must mention here one more caveat regarding the pump manager that is often true of methods that use notification objects and return values. Because a notification object is used, the return value from the getPump method occurs outside of the synchronized block for the "this" object. This is very dangerous, and care should be taken when setting the return value. Technically, this program has a race condition. For example, suppose Car 1 arrives at the gas station followed by Car 2, and the pumps are then freed in the order of Pump 1 then Pump 2. Our intent is to give Pump 1 to Car 1, and Pump 2 to Car 2; however, in the program as written it is possible for Car 1 to get Pump 2 and Car 2 to get Pump 1. This is not a real problem in this simulation because the pump manager gives a free pump to the cars as pumps become free. However, it does show that a race condition is present because of the notification object locking.

Exhibit 12: Program9.4c: Pump Class

start example

 import java.util.Vector; import java.util.Enumeration; class Pump {   private Vector pumpListeners = new Vector();   /**    * Method to add a listener to the pump    */   public void addPumpListener(PumpListener pl) {     pumpListeners.addElement(pl);   }   /**    * Method to remove a listener from the pump    */   public void removePumpListener(PumpListener pl) {     pumpListeners.removeElement(pl);   }   /**    * This is the process event method for the pump event. It is    * called when the pump is free.    */   public void notifyPumpIsFree() {     PumpEvent evt = new PumpEvent(this, this);     Vector v;     synchronized(this) {       v = (Vector) pumpListeners.clone();     }     Enumeration e1 = v.elements();     while (e1.hasMoreElements()) {       PumpListener P = (PumpListener)(e1.nextElement());       P.pumpReady(evt);     }   } /**    * Method that turns the pump on. It lets any listeners    * know that the pump is now available to use.    */   public void startPump() {     notifyPumpIsFree();   }   /**    * Method to simulate pulling up to the pump    */   public void usePump() {     try {       // Simulate pulling to the pump by waiting .5 second.       Thread.sleep(500);     } catch (InterruptedException e) {     }   }   /**    * Method to simulate pumping the gas    */   public void pumpGas() {     try {       // Simulate pumping gas by waiting 5 seconds.       Thread.sleep(5000);     } catch (InterruptedException e) {     }   }   /**    * Method to simulate leaving the pump    */   public void leave() {     try {       // Simulate leaving the pump by waiting .5 second.       Thread.sleep(500);       notifyPumpIsFree();     } catch (InterruptedException e) {     }   } } 

end example

In this case, if the pump given to a car did matter, another solution would have to be sought. For example, the notification object could be changed to store the correct pump, and that pump could be set in the synchronized block of the pumpReady method of the Pump manager. This pump could be retrieved in the getPump method. Because the notification object has a direct correspondence to a specific car thread, this would guarantee that the correct car is given to the correct thread. The details of actually implementing this are left as an exercise.

9.5.1 Adapters

The concept of an adapter was mentioned in the context of the PumpManager object. An adapter is a very important type of object in component programming. It is not a component, but an adapter is an object that changes the plumbing (so to speak) of an object so that the behavior of the object more closely matches the needs of the problem to be solved. Adapters have real-world uses, and most of these have analogs in OOP. For example, in plumbing a 1/2-inch pipe cannot be directly connected to a 1/4 pipe; an adapter would have to be used to change the diameter of one of the pipes. Another use for an adapter is to split a single input into multiple outputs, such as is accomplished by a cable splitter, which is used to break the input from a single cable input line into multiple lines that can be used on multiple televisions in a home. Adapters can also bring multiple inputs together into a single output, such as all the drains in a home connecting to form one output line to the local sewer. A final example of an adapter is one that can take multiple input lines and filter them in some manner — for example, by choosing one of them, much as a radio or television selects from all the different radio wave or cable frequencies being received.

Exhibit 13: Program9.4d: Car Class

start example

 import java.util.Date; import java.util.Random; publicclass Car implements Runnable {   private static int totalTime = 0,// Total time spent by all                 // cars                          totalCars = 0;// Total number of cars   private int customerNumber;   private Pump myPump;   private PumpManager pumpManager;   /**    *  Public constructor. This constructor sets the    *  customerNumber for output and sets the pumpManager    *  so that it can talk to the adapter component.    */   public Car(int customerNumber, PumpManager pumpManager) {     this.customerNumber = customerNumber;     this.pumpManager = pumpManager;   }   /**    *  At the end of the simulation, this method is used to    *  calculate the average time taken by all the cars in the    *  simulation.    */   public static float calcAverage() {     return totalTime/totalCars;   }   /**    * Run method, where the behavior of the car in the    * simulation is defined.    */   public void run() {     final int WAIT_TIME = 30000;     long startTime, endTime;     Random random = new Random(customerNumber);     int waitTime = random.nextInt(WAIT_TIME);     // Wait a random amount of time before coming to pump.     try {       Thread.sleep(waitTime);       startTime = (new Date()).getTime();       // Wait for Pump Manager to tell me a pump is free.       System.out.println("Customer" + customerNumber +           " waiting in line");       myPump = pumpManager.getPump();       System.out.println("Customer" + customerNumber +           " arrives at pump");       myPump.usePump();       System.out.println("Customer" + customerNumber +           " pumps gas");       myPump.pumpGas();       System.out.println("Customer" + customerNumber +           " leaves pump");       myPump.leave();       endTime = (new Date()).getTime();       System.out.println("Time =" + (endTime - startTime));       totalTime + = (endTime - startTime);       totalCars++;     } catch (InterruptedException e) {     }   } } 

end example

Before discussing adapters further, it is important to realize that the use of an adapter can result in other problems that have to be solved. For example, an adapter designed to connect a 1/2-inch pipe to 1/4-inch pipe would also have to handle problems associated with pressure differences resulting from the change in pipe diameter. The cable splitter may have to include a repeater to account for the loss of signal attenuation. Just as adapters have counterparts in OOP, adapters used in OOP also have problems that must be addressed when they are implemented.

Adapters have many uses in OOP that often correspond to the use of adapters in the real world. For example, to use an existing object in a new environment it might be necessary to map the interface methods of the existing object to the new methods that will work in the new environment. In this case, a separate adapter object could be created that would provide the connection between the object and the new environment, much as the plumbing adapter connects the 1/2-inch and 1/4-inch pipes. Another example of an adapter in OOP would be a single object that listens to the events generated from a process, such as the temperature controller on a chemical reactor. This object could filter the temperature events, looking for ones where the temperature exceeds some limit, and then could multicast that event to a number of listeners. This would save time, as only the events of interest would be generated to all the listeners, and the check for temperature deviations could occur once in a single object.

In the gas station example, the pump manager is an example of an adapter in that it listens to all of the possible pumps (acting as a de-multiplexer) and then assigns a car to the pump (acting as a filter on the pumps). This use of an adapter to the pump event is important, as otherwise the cars would have to listen to all the events from all the pumps and choose from among themselves who will be allowed to act in response to the event. This scenario would be complicated to program and would result in significant runtime overhead.

Exhibit 14: Program9.4e: PumpManager Class

start example

 import java.util.Vector; public class PumpManager implements PumpListener {   private Vector customers = new Vector();   private Vector pumps = new Vector();   private int pumpsAvailable = 0;   /**    * This method is called by a car thread to get a Pump object    * that it can use. If no Pump object is available, the car    * thread will wait until a pump manager has a pump    * available. The car threads are notified in the order in    * which they begin waiting.    */   public Pump getPump() {     Object notifyObject = new Object();     synchronized (notifyObject) {       synchronized(this) {           pumpsAvailable — ;           if (pumpsAvailable < 0) {             customers.addElement(notifyObject);           }           else {             notifyObject = null;           }       }       if (notifyObject ! = null)         try {           notifyObject.wait();         } catch (InterruptedException e) {       }     }     return (Pump)(pumps.remove(0));   }   /**    * This method is called by the Pump object when it becomes    * free (i.e., it starts or a car leaves). Note that this    * method must consider the cases where a customer (car) is or    * is not waiting.    */   public synchronized void pumpReady(PumpEvent e) {     pumps.addElement(e.getPump());     pumpsAvailable++;     if (!customers.isEmpty()) {       Object notifyObject = customers.remove(0);       synchronized(notifyObject) {         notifyObject.notify();       }     }   } } 

end example

Exhibit 15: Program9.4f: GasStation Class

start example

 public class GasStation {   public static final int NUMBER_OF_CUSTOMERS = 10;   public static void main(String args[]) {     Pump pump1 = new Pump();     Pump pump2 = new Pump();     PumpManager pm = new PumpManager();     pump1.addPumpListener(pm);     pump2.addPumpListener(pm);     pump1.startPump();     pump2.startPump();     Thread threadArray[] = new Thread[NUMBER_OF_CUSTOMERS];     for (int i = 0; i < NUMBER_OF_CUSTOMERS; i++) {       threadArray[i] = new Thread(new Car(i, pm));       threadArray[i].start();     }     for (int i = 0; i < NUMBER_OF_CUSTOMERS; i++) {       try {         threadArray[i].join();       } catch(InterruptedException e) {       }     }     // Print average time at the end of the simulation.     System.out.println("Average time to get gas = "+       Car.calcAverage());   } } 

end example



 < Day Day Up > 



Creating Components. Object Oriented, Concurrent, and Distributed Computing in Java
The .NET Developers Guide to Directory Services Programming
ISBN: 849314992
EAN: 2147483647
Year: 2003
Pages: 162

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net