7.5 Implementing a Generic Animator

 < Day Day Up > 



7.5 Implementing a Generic Animator

The animator in Exhibit 4 (Program7.2) has two basic problems. The first is that it caused the GUI components, such as buttons, to behave poorly because it called Thread.sleep inside of the paint method in the GUI thread. The second problem is that the animator is specific to the Ball program. To use it in another program it must be copied and modified to work with the new program. A better way to develop the animator would be using the techniques in Chapter 5 to create a generic component that can be used with many different types of objects and in many different programs.

Exhibit 5: Program7.4: Generic Animator with Control Panel

start example

 import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; public class Animator extends JPanel implements Runnable {   private Vector elementsToDraw = new Vector();   private long sleepTime = 100;   private boolean animatorStopped = true, firstTime = true;   private ControlFrame controlFrame;   private JFrame animFrame;   /**    *  Constructor that creates the JFrame for the animator. Note    *  that the JFrame is shown in the show() method because this    *  starts the GUI thread. Starting the thread in the    *  constructor can lead to a race condition.    */   public Animator() {     // Create the control Frame.     controlFrame = new ControlFrame(this);     // set up the frame to draw in.     animFrame = new JFrame("Generic Animator");     Container animContainer = animFrame.getContentPane();     animContainer.add(this);     animFrame.setSize(700, 450);     animFrame.setLocation(0,100);   }   /**    *  setVisible is called to display or hide the animator. Note    *  that only display = true is implemented, and this function    *  only works at this point if it is called once. It is left    *  as an exercise to implement it correctly. If display =    *  false, the Control Thread must be suspended. If    *  display = true, the control thread should be started only    *  the first time; after that it should be unsuspended. This    *  can be accomplished by using control variables in the    *  paint method for Program7.4 and after and should not be    *  done using the suspend and resume methods.    */   public void setVisible(boolean display) {     if (display = = true) {       if (firstTime) {         firstTime = false;         // Show the control Frame         controlFrame.setVisible(true);         // Show the animator. This starts the GUI thread.         animFrame.setVisible(true);         // Put the animator in another thread so that the         // calling object can continue.         (new Thread(this)).start();       }     }   }   /**    *  Thread that runs the animator. This thread sleeps for some    *  amount of time specified by sleepTime, then calls repaint,    *  which forces a call to the paint method, but in the GUI    *  thread. Note that the animatorStopped button allows the    *  animator to single step and pause the animation. The    *  notify is done in the control frame from the button.    */   public void run() {     while (true) {       try {         synchronized(this) {           if (animatorStopped = = true) {             wait();           }         }         if (animatorStopped ! = true)           Thread.sleep(sleepTime);       } catch (InterruptedException e) {         System.out.println("Program Interrupted");         System.exit(0);       }       repaint();     }   }   /**    *  The paint method redraws all the objects registered with    *  this animator. It is run in the GUI thread. The repaint    *  command causes this method to be called, and it passes the    *  graphics object g to each of the registered objects to    *  have them draw themselves.    */   public void paint(Graphics g) {     super.paint(g);     Enumeration e = elementsToDraw.elements();     while (e.hasMoreElements())       ((Drawable) e.nextElement()).draw(g);   }   /**    *  addElement adds each drawable to the vector for use by the    *  DrawElements method.    */   public void addDrawable(Drawable d) {     elementsToDraw.addElement(d);   }   /**    *  removeElement is used to remove drawables from the vector.    */   public void removeDrawable(Drawable d) {     elementsToDraw.removeElement(d);   }   /**    *  This is an inner class which implements the control panel    *  for the application. It is not important to the behavior    *  of the animator component, which can be understood without    *  understanding this control panel.    *    *  Note that we do not need to synchronize any of these    *  methods because the values they change can only be changed    *  in the GUI thread and are read only elsewhere, so no race    *  conditions exist.    */   private class ControlFrame {     Animator ca;     static final int RUNNING = 0;     static final int WAITING = 1;     int state = WAITING;     JFrame controlFrame;     public ControlFrame(Animator Parent) {       ca = Parent;       controlFrame = new JFrame("Controller");       Container controlContainer = controlFrame.getContentPane           ();       controlContainer.setLayout(new FlowLayout());       final JButton startButton = new JButton("Start");         startButton.addActionListener(new ActionListener() {         public void actionPerformed(ActionEvent e) {           if (state = = RUNNING) {             state = WAITING;             startButton.setText("Start");             ca.animatorStopped = true;           }           else {             state = RUNNING;             startButton.setText("Stop");             synchronized (ca) {               ca.animatorStopped = false;               ca.notify();             }           }         }       });       controlContainer.add(startButton);       final JButton stepButton = new JButton("Step");       stepButton.addActionListener(new ActionListener() {         public void actionPerformed(ActionEvent e) {           if (state = = RUNNING) {             state = WAITING;             startButton.setText("Start");             ca.animatorStopped = true;           }           synchronized (ca) {             ca.notify();           }         }       });       controlContainer.add(stepButton);       JButton exitButton = new JButton("Exit");       exitButton.addActionListener(new ActionListener() {         public void actionPerformed(ActionEvent e) {           System.exit(0);         }       });       controlContainer.add(exitButton);       controlContainer.add(new Label("Animator Speed"));       final JScrollBar speedControl = new JScrollBar(JScrollBar.           HORIZONTAL, 500, 25, 100, 1000);       speedControl.addAdjustmentListener(new AdjustmentListener           () {         public void adjustmentValueChanged(AdjustmentEvent e) {           ca.sleepTime = 1000 - e.getValue();         }       });       controlContainer.add(speedControl);       controlFrame.setSize(500, 100);     }     public void setVisible(boolean show) {       controlFrame.setVisible(show);     }   } } 

end example

The animator presented in Exhibit 5 (Program7.4) solves both of these problems. The changes to make it generic and to add a separate thread to handle delays are limited in number but involve interactions between three threads and so are somewhat complex. The changes to implement the control panel are a relatively straightforward implementation of a Java GUI; however, as with most GUI components, they require a fair amount of code. Therefore, the generic animator is developed here in two steps. The first step, shown in Exhibit 6 (Program7.3a) and used in Exhibit 7 (Program7.3b), implements the basic generic animator that allows objects of nearly any type to be animated; however, it does have a fixed time delay, like that shown Exhibit 2 (Program7.1b), so that the complexity of the control panel does not have to be considered (see Sections 7.5.1 and 7.5.2). Section 7.5.1 explains the basic design of the animator and how interfaces can be used to implement a generic solution to make the animator able to handle many objects of different types. Section 7.5.2 explains how the threads that are present interact to make the animator work.

Exhibit 6: Program7.3a: The Drawable Interface

start example

 import java.awt.*; /**  *  Purpose: The Drawable interface allows objects to register  *           with the animator. It defines one method, a draw  *           method, that takes a single parameter, a Graphics  *           object.  */ interface Drawable {   public void draw(Graphics g); } 

end example

Exhibit 7: Program7.3b: The Generic Animator

start example

 import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; public final class Animator extends JPanel implements Runnable {   Vector elementsToDraw = new Vector();   long sleepTime = 100;   boolean animatorStopped = true, firstTime = true;   JFrame animFrame;   /**    *  Constructor that creates the JFrame for the animator. Note    *  that the JFrame is shown in the show() method because this    *  starts the GUI thread. Starting the thread in the    *  constructor can lead to a race condition.    */   public Animator() {     animFrame = new JFrame("Generic Animator");     Container animContainer = animFrame.getContentPane();     animContainer.add(this);     animFrame.setSize(700, 450);     animFrame.setLocation(0,100);   }   /**    *  setVisible is called to display or hide the animator. Note    *  that only display = true is implemented, and this function    *  only works at this point if it is called once. It is left    *  as an exercise to implement it correctly. If display =    *  false, the Control Thread must be suspended. If    *  display = true, the control thread should be started only    *  the first time; after that it should be unsuspended. This    *  can be accomplished as using control variables in the    *  paint method for Program7.4 and after and should not be    *  done using the suspend and resume methods.    */   public void setVisible(boolean display) {     if (display = = true) {       if (firstTime) {         firstTime = false;         // Show the animator. This starts the GUI thread.         animFrame.setVisible(true);         // Put the animator in another thread so that the         // calling object can continue.         (new Thread(this)).start();       }     }   }   /** Thread that runs the animator. This thread sleeps for some    *  amount of time specified by sleepTime, then calls repaint,    *  which forces a call to the paint method, but in the GUI    *  thread. Note that the animatorStopped button allows the    *  animator to single step and pause the animation. The    *  notify is done in the control frame from the button.    */   public void run() {     while (true) {       try {         Thread.sleep(100);       } catch (InterruptedException e) {         System.out.println("Program Interrupted");         System.exit(0);       }       repaint();     }   }   /**    *  The paint method redraws all the objects registered with    *  this animator. It is run in the GUI thread. The repaint    *  command causes this to be called, and it passes the    *  graphics object g to each of the registered objects to    *  have them draw themselves.    */   public void paint(Graphics g) {     super.paint(g);     Enumeration e = elementsToDraw.elements();     while (e.hasMoreElements())       ((Drawable) e.nextElement()).draw(g);   }   /**    *  addElement adds each drawable to the vector for use by the    *  DrawElements method.    */   public void addDrawable(Drawable d) {     elementsToDraw.addElement(d);   }   /**    *  removeElement is used to remove drawables from the vector.    */   public void removeDrawable(Drawable d) {     elementsToDraw.removeElement(d);   } } 

end example

The second stage of the animator involves adding the ControlPanel. This is done in Exhibit 5 (Program7.4), and explained in Section 7.5.3. It will allow a user to control the speed of an animation, stop the animation, or single step the animation. The change to the animator itself is minor, but implementing the ControlPanel requires a lot of standard Java GUI code, and would have been confusing in the amount of detail if presented with Exhibit 6 (Program7.3a).

The animator produced in Exhibit 5 (Program7.4) is a large improvement over the one in Exhibit 9, but unfortunately it is not a final product because a race condition arises when adding or removing objects from the animator. This problem is explained in Section 7.5.4, and solved by implementing the Java Event Model for this animator in Section 7.6.

7.5.1 The Basic Design of the Animator

The first problem to be solved is creating a generic animator that can animate many objects of many different types. Chapter 5 showed that the correct way to do this in Java is to write components using interfaces; then an algorithm or component (in this case, an animator) can operate on any object that implements this interface. So, the first step in this process of writing a generic animator is to create an interface. For the animator, the interface created will be a Drawable, as shown in Exhibit 6 (Program7.3a). It has a single method, draw, which takes as a parameter an object of type Graphics.

This definition of the draw method is called with the Graphic object from the paint method in the animator. The animator is only responsible for handling repaint events and calling this draw method. Determining what to draw and where to draw it is the responsibility of the object being animated. So, if the Drawable implements a path and draws an image at the points given by the path on the Graphic object passed from the animator's paint method, it will in effect be animated by the animator.

The animator that uses this Drawable interface is shown in Exhibit 7 (Program7.3b). This animator is different from the animator in Exhibit 4 (Program7.2) in that it is not designed to implement as part of a single program but is to be used generically in many different programs. The basic design is that objects that are to be animated declare that they are Drawable, or they have a draw method and can be drawn and used in the animation. These objects then register with the animator using the addDrawable method in the animator. If an object no longer wishes to be a part of the animation, it is passed to the removeDrawable method (see step 1 in Exhibit 8). The animator then has repaint events generated (see Section 7.5.2). When these repaint events occur, the animator calls the draw method in all the registered objects passing the Graphic object (see step 2 in Exhibit 8).

Exhibit 8: Deadlock in an Animator

start example

click to expand

end example

The animator implements the design in Exhibit 8 as follows. The animator contains a vector, called ElementsToDraw, which keeps track of objects that are to be drawn with the animator. When the addDrawable method is called, the Drawable object passed to it is added to the ElementsToDraw (thus being added to the animation), and when the removeDrawable method is called the object is removed (thus being removed from the animation). Each time a repaint event is scheduled, the paint method in the animator is called, and the paint method in turn calls the draw methods of all the objects that are registered with the animator (all objects in the ElementsToDraw vector). Each object draws on the Graphic object, and the result is that all the registered objects are displayed in the animator. The part of the paint method that allows Drawable objects to draw on the Graphics object is given below:

   Enumeration e = elementsToDraw.elements();   while (e.hasMoreElements())     ((Drawable) e.nextElement()).draw(g); 

Note that each time paint is called (each 0.1 seconds) the draw methods of all registered elements are called.

To better understand how this works, consider Exhibit 9 (Program7.3c), which uses this animator. To use the animator, an instance of the Animator is first created, then the objects to be drawn as part of the animator are created and added to the animator. This is shown in the following five lines of code, which implement the heart of the MoveObjects program in Program7.3 (Exhibits 6, 7, and 9):

   Animator animator = new Animator();   MoveBall mb = new MoveBall(animator, MoveBall.UP, 1);   animator.add(mb);   MoveRect mr = new MoveRect(animator, MoveRect.DOWN, 2);   animator.add(mr); 

Exhibit 9: Program7.3c: Program That Uses the Generic Animator

start example

 import java.awt.*; public class MoveObjects {   public static void main(String args[]) {     Animator animator = new Animator();     MoveBall mb = new MoveBall(MoveBall.UP, 1);     animator.addDrawable(mb);     MoveRect mr = new MoveRect(MoveRect.DOWN, 2);     animator.addDrawable(mr);     animator.setVisible(true);   } } /**  *  Purpose: This class implements a ball that moves itself on  *           subsequent call to the draw method.  */ class MoveBall implements Drawable {   static final int UP = 1;   static final int DOWN = 0;   private int direction = DOWN;   private Path myPath;   private Point myPosition = new Point(10,10);   private int myNumber;   /**    *  Constructor, simply save the initial values.    */   public MoveBall(int direction, int myNumber) {     this.direction = direction;     this.myNumber = myNumber;   }   /** Draw is called each time through the animator loop to draw    *  the object. It uses the path to calculate the position of    *  this object and then draws the object at that position.    */   public void draw(Graphics g) {     if (myPath ! = null && myPath.hasMoreSteps())       myPosition = myPath.nextPosition();     else {       // Get a random number of steps to make the balls move       // at different speeds. Note there has to be at least       // one step in each path, but for appearances we used at       // least ten steps.       int numberOfSteps = (int) (10.0 + (Math.random() * 100.0));       if (direction = = DOWN) {         myPath = new StraightLinePath(410, 410, 10, 10,           numberOfSteps);         myPosition = myPath.nextPosition();         direction = UP;       }       else {         myPath = new StraightLinePath(10, 10, 410, 410,            numberOfSteps);         myPosition = myPath.nextPosition();         direction = DOWN;       }     }     g.setColor(Color.RED);     g.fillOval((int)myPosition.getX(), (int)myPosition.getY(),       15, 15);     g.setColor(Color.BLACK);     g.drawString("" + myNumber,       (int)myPosition.getX()+3, (int)myPosition.getY()+12);   } } /**  *  Purpose: This class implements a rectangle that moves  *           itself on subsequent call to the draw method.  */ class MoveRect implements Drawable {   static final int UP = 1;   static final int DOWN = 0;   private int direction = DOWN;   private Path myPath;   private Point myPosition = new Point(10,10);   private int myNumber;   /**    *  Constructor simply saves the initial values.    */   public MoveRect(int direction, int myNumber) {     this.direction = direction;     this.myNumber = myNumber;   }   /**    *  Draw is called each time through the animator loop to draw    *  the object. It uses the path to calculate the position of    *  this object and then draws the object at that position.    */   public void draw(Graphics g) {     if (myPath ! = null && myPath.hasMoreSteps())       myPosition = myPath.nextPosition();     else {       // Get a random number of steps to make the balls move       // at different speeds. Note there has to be at least       // 1 step in each path, but for appearances we used at       // least 10 steps.       int numberOfSteps = (int) (10.0 + (Math.random() * 100.0));       if (direction = = DOWN) {         myPath = new StraightLinePath(410, 410, 10, 10,             numberOfSteps);         myPosition = myPath.nextPosition();         direction = UP;       }       else {         myPath = new StraightLinePath(10, 10, 410, 410,             numberOfSteps);         myPosition = myPath.nextPosition();         direction = DOWN;       }     }     g.setColor(Color.YELLOW);     g.fillRect((int)myPosition.getX(), (int)myPosition.getY(),         15, 15);     g.setColor(Color.BLACK);     g.drawString("" + myNumber,       (int)myPosition.getX()+3, (int)myPosition.getY()+12);   } } 

end example

The specific classes MoveBall and MoveRect implement the Drawable interface. So, when added to the animator (via the animator.add method), they are contained in the elementsToDraw vector and will have their draw methods called every 0.1 seconds. In their draw methods, they define a path to keep track of what to draw at what positions. Thus, every 0.1 seconds they draw on the animator's Graphics object, which is displayed on the terminal, giving the appearance of movement.

Exhibit 9 (Program7.3c) shows that the animator is generic in two ways. First, the animator is separated from the types of objects that can be used, as any object that implements Drawable can be used by the animator. Program7.3 (Exhibits 6, 7, and 9) could animate both Ball and Rect objects. Second, the animator is separate from the program that uses it, as any program can create an animator object and then add Drawable objects to it. This is a big advantage over Exhibit 4 (Program7.2), where the animator could only animate specific objects and need to be copied and modified to be used in other programs.

7.5.1.1 Extending the Class Vector

One final point must be considered in regard to the design of the generic animator shown in Exhibit 7 (Program7.3b). Some programmers would feel that the addDrawable and removeDrawable methods are redundant, as all they do is call the addElement and removeElement methods in the Vector class, and it would be better for the animator to extend the Vector class, thus allowing for reuse of all the methods in the Vector. However, the Java AWT indicates that this is not the way the implementers of the AWT chose to implement AWT components, such as Button with addActionListener or Textfields with addTextListener, which should make these programmers wonder why not. The reasons that extending class Vector is a bad idea are covered in more detail in Chapter 10; however, it can be pointed out here that one problem that makes this strategy unworkable is if class Vector is extended then the addElement call can take objects of any type, not just objects of classes implementing Drawable. Using the animator in Exhibit 7 (Program7.3b), if a programmer attempted to add an object that was not a Drawable, a compiler error would be generated. If, instead, the class Vector was extended, the adding of a non-Drawable object would not be caught until runtime when the JVM was attempting to cast the object in the paint method. Given a choice, it is always better to catch an error at compile time than runtime, so making the animator extend the class Vector is a bad solution.

Exhibit 10: Design of the Generic Animator

start example

click to expand

end example

7.5.2 How the Repaint Events Are Generated

This section explains in detail how the animator in Exhibit 7 (Program7.3b) actually generates the repaint events, thus creating the animation. This requires only about 25 lines of code but involves coordinating between three threads, two of them implicit, so it is important that the interactions be carefully considered and understood. The three threads are the main thread, which is started when the program is started; a GUI thread, which is started when the JFrame is created; and a thread started when the animator's setVisible method is called, which controls the repaint event and is called the control thread.

The Animator object is short but very complex because at some point in time each of the three threads can be executing in methods in the object. For example, the main thread runs the animator constructor when it first creates the animator. Once the animator is created, the main thread adds and removes objects from the animator by calling the addDrawable and removeDrawable methods, thus these methods are also run from the main thread. The GUI thread is running once the JFrame is created and made visible, and each time the JFrame is repainted the paint method of the animator is called by the GUI thread. This is as expected because the paint method and any listener methods are always run in the GUI thread. Finally, the control thread started in the setVisible method starts in the run method for the animator object. Because it consists entirely of an infinite loop calling the Thread.sleep and repaint methods, it is always running in a method in the animator. So, at some point in time all three threads are executing in methods in the animator object.

The reason the control thread was added to the design is that it moves the sleep out of the paint method (thus, out of the GUI thread) and into a completely separate thread, thus allowing the GUI thread to still run and process events while the control loop is sleeping. Now the animator will work correctly when a control panel with scroll bar and buttons is added, because the control thread is sleeping and generating the repaint events, which allows the GUI thread to run and process events from the buttons and scrollbar in an expeditious manner.

The fact that multiple threads are running in the animator object can cause many problems to programmers who do not carefully consider how the threads in the program interact. For example, some programmers are so afraid of race conditions that they automatically synchronize all methods in an object. Doing so creates an immediate problem in this object, as synchronizing the run method causes the control thread to keep the object locked and does not allow the GUI thread or the main thread to ever execute calls in the animator. So, the question of which methods, or parts of methods, really must be synchronized must be examined carefully. As pointed out in Section 7.5.4, a race condition does exist in this component, and some form of locking needs to be added; however, unlike the programs in Chapter 3, the required synchronization must be more carefully considered and can become quite complex. That is why it is important for programmers to understand the issues involved and to choose the simplest means to coordinate programs that works effectively.

7.5.3 Adding the Component Controls

Now that the basic animator is in place, a control frame can be created to do such things as single stepping through the animation or controlling the animation speed. Because the entire control of the animator is in the run method for the Animator class, a controller can be created so that only two methods in the Animator class require minor modifications from Program7.5 (Exhibits 12 through 15). These modifications are in the constructor and the run method. The actual creation of the controls is put in a separate class named ControlFrame, and except for the variables animatorStopped and sleepTime it is completely isolated from the Animator Class.

The ControlFrame class is a standard Java GUI, so the issues involved in writing it are not really germane to the creation of the component. A more detailed explanation of how to implement a Java GUI can be found in any number of books on programming in Java. The only results from the ControlFrame are to set the sleepTime and animatorStopped variables. The sleepTime is the amount of time to wait between calls to repaint when the animator is running and thus controls how quickly the animation runs. The animatorStopped works by having the control thread call wait if animatorStopped is true, and the animation only continues when the ControlFrame calls notify to wake up the control thread. This allows the animation to be started and stopped, as well as single stepped.

The changes that need to be made in the Animator class are shown in Exhibit 5 (Program7.4). In the animator's constructor a Control Frame object is created. Also, in the run method the amount of time to sleep has been changed from a constant to a variable that can be set in the Control Frame. Also, a wait condition if the animatorStopped variable is true has been added; otherwise, the Animator class and the basic animator design are unchanged from the version shown in Exhibit 7 (Program7.3b). This version of the animator has the same external interface as Exhibit 7 (Program7.3b), so the same MoveObject program in Exhibit 9 (Program7.3c) can be run in the new version of the animator.

7.5.4 Minimalist Approach to Design

The design of the animator is minimalist in that the animator only creates and propagates the paint events. It does not include all the behavior needed to draw object and images nor is this behavior necessary to calculate and move objects contained in the animator. The fact that the behavior will be available is guaranteed by the Drawable interface; however, exactly how to implement that behavior is left up to the object to be drawn.

By using an interface and making the objects responsible for what and where to draw, the animator allows Drawable objects to do anything in their draw methods, from drawing stick figures using circles and lines to generating and displaying complex images; indeed, anything that can be done with a Graphics object can be displayed with this animator. The animator does not have to be extended to provide this functionality. This makes the animator move cohesive.

The use of the Drawable interface also allows the animated object to decide how to move. This means that if the object chooses not to use a path, or to use a complex path with a Bessel function or Spline Curve, the animator does not have to be modified to allow for this new movement behavior. Even complex behaviors, like balls bouncing off of each other or off the sides of the screen, can be implemented in the animated objects, and would not cause the animator to be modified in any way. This reduces the coupling between the animator and the objects that implement it.

This minimalist approach to implementing the animator is what makes it more cohesive and less tightly coupled to the program. This is important because it allows applications that use the animator to be designed to solve the problems they are intended to solve and not to follow rules imposed by the animator. In fact, entire applications written without considering how to animate them can have animation added as a feature, as shown in Chapter 8 when programs from Chapter 3 are animated. The animator can even be used, unmodified, with objects distributed on a network. Because all of the behavior of the objects is part of the application that uses the animator, and not built into the animator itself, the animator can be used with a wide variety of applications, making it a much more powerful component than its simple design might imply.

This is an important aspect of this design. Often programmers are tempted to make a component such as an animator a single, monolithic entity that implements all the behavior that is possible using that component. In this case, this would mean that the animator would not call draw methods in the objects that are animated, but instead would implement the mechanisms to draw the objects in the animator itself. Any other type of behavior, such as how the objects move or interact, would also be built into the animator. When this happens, large, complex components are created. A simple argument would suggest that these components are tightly coupled with the applications that use them, as much of the behavior required by the application is contained in the animator and not in the objects. Also, the cohesion of the animator is lost, as it now has to perform many duties other than calling draw methods in the using objects. So, a cursory argument would say a monolithic animator is not as good an option as the generic animator in Program7.4 (Exhibit 5). That said, anyone who has dealt with a large, complex system that tries to do anything should be able to appreciate the simplicity of the animator given in Exhibit 5 (Program7.4).

Not making the Animator a single, monolithic entity has a number of other advantages as well. First, the Animator is simple and thus easier to use, and it probably contains fewer errors. This is particularly important when using concurrency because programming with concurrency can have a number of unanticipated consequences. Second, the animator is more likely to be usable by other programs because much of the behavior is provided by the user program and not enforced by the animator, so an application can be written to the constraints of the application and the concurrency added as a separate feature by implementing a draw method in the object. Finally, because the Animator provides only the minimum functionality, new functionality (such as drawing shapes or images not originally envisioned when the animator was created) does not involve modifying a large, complex animator with all the version control, debugging, testing, and distribution problems doing so would entail. This allows the animator to be extended and used in situations beyond those it was originally envisioned to handle.

7.5.5 Race Condition in the Generic Animator

The generic animator in Exhibit 5 (Program7.4) is a big improvement over the animator in Exhibit 4 (Program7.2); however, it still has a problem that needs to be fixed: a race condition in the interaction between adding and removing objects from the elementsToDraw vector and the walking of elementsToDraw in the paint method to call each object's draw method. This race condition exists in spite of the fact that the Vector class that the elementsToDraw instantiates is synchronized completely (the results of running any method that modifies or uses data in the Vector will be in a synchronized block). In fact, each call to a method in the Vector is safe in that a call to addElement cannot be run at the same time as another call to removeElement. The possible race condition occurs when the Vector is assumed to be safe between calls, as when it is used in an enumeration in the paint method.

To see this race condition, assume that the paint method is currently calling each of the objects in the elementsToDraw. In the middle of the list, the GUI thread is suspended, and the main thread runs and adds or removes an object from the elementsToDraw. When the GUI thread later resumes, the item added or removed has shifted the elements in the elementsToDraw, and this can cause one of two problems. If the items are shifted down one element, it is possible that an object whose draw method has already been called will be called a second time. If the items have been shifted up, it is possible to skip the call to the draw method for an object. In either case, the result is incorrect. If an Iterator is used instead of an Enumeration, the program would throw a ConcurrentModificationException when this situation occurs; however, as stated in the API for an Iterator, this exception cannot be relied on, and even if it could be relied on it does nothing to help fix the problem that a Drawable object could be drawn twice or not at all. The animator has to be fixed to deal correctly with this problem.

One simple fix would be to simply synchronize all the methods that have access to the elementsToDraw vector. This would require that the addDrawable, removeDrawable, and paint methods be synchronized, which has two drawbacks. The first is that an addDrawable or removeDrawable call should be quick, but if the calls to the draw methods of the elementsToDraw objects from the paint method are slow (for example, if the objects were to exist in a distributed computing environment), then the addDrawable and removeDrawable methods would have to wait, which would result in poor performance.

A more serious problem, however, is the possibility of deadlock. Exhibit 10 shows how this could occur. Assume that a Drawable object named object_1 exists, and it has two methods, method_1 and draw, both of which are synchronized. At some point earlier in the program, object_1 has registered with the animator using the addDrawable method and now wishes to remove itself from the animation by calling removeDrawable. The thread running in object_1's method_1 now has the lock on object_1 and is attempting to obtain the lock on the animator. At the same time, a repaint event has occurred, and the paint method in the animator is being run. The GUI thread in paint now has the lock on the animator and is attempting to obtain the lock on object_1's draw method. Obviously, neither can proceed, and deadlock has occurred.

Because the time scale for the animator is so large, on the order of milliseconds, it is likely that it will run for a very long time before this problem occurs, but the problem is still there and could arise at any time, often at a most unfortunate time. These types of problems are extremely subtle and difficult to reason through, and nearly impossible to test for and debug after the fact; therefore, it is important to use a pattern to implement the type of event dispatching that is known to be correct. This can be done using the Java Event Model, which is applied to the animator in Section 7.6 to produce an animator that is correct.



 < 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