Threads and applets and visual controls

The last part in the series explores
reading multiple data channels

Before jumping into this final column on threads and inter-applet communication let's review a bit of what we've covered so far. In "Synchronizing threads in Java" I looked at synchronizing two or more threads using the wait and notify methods of the Object class. In "Using threads in Java, Part II" I took the synchronized threads and created an in-memory communication channel so that two or more threads could exchange object references. Then in "Using communication channels in applets, Part 3" I applied these communication channels to co-operating applets that were laid out in an HTML page by a Java-aware browser. These applets combined to create an active HTML page that implemented a simple temperature conversion applet.

In this last column on threads and interapplet communication I'll look at a couple issues associated with this approach, and in particular I'll discuss how a layer can be created on top of the existing DataChannel design to allow multiple DataChannels to feed into a single DataChannel. The source code is also online and available for your use, in either an elaborate form, or as a tar or zip archive.

Next month I'll pick on another oft-misunderstood aspect of the Java system, class loaders.

Choice control

The choice control is a common and useful control for applications. You use these controls when you want an application to accept one of several choices and the choices are well specified. Examples of such choices are "true or false" and "strongly agree, agree, neutral, disagree, strongly disagree." To implement a choice control, you must specify two properties to be satisfied:

  • The control must know when it is selected.
  • All of the controls in the group must know when they are not selected.

We implement this easily with a DataChannel.

The skeleton of the OptionButton control is as follows:

 1  public class OptionButton extends Applet implements Runnable {
 2      public void init() {
 3      new DataChannel(getparameter("datachannel");
 4      state = false;
 5      }
 6      public void paint(Graphics g) {
 7      ... display my label and my choice ring ...
 8      }
 9      public void start() { ... create the data channel ... }
10      public void stop() { ... release the data channel ... }
11      public void run() {
12      while (thread == currentthread) {
13          value = getValue();
14          current_state = value == myValue;
15          repaint();
16     }
17      }
18      public boolean mouseUp(...) { sendValue(myID); }
19  }

As you can read in the code, the applet is quite simple. The magic is taken care of by the DataChannel. The basic applet knows only two things -- how to render itself in the checked and unchecked state -- and when it gets clicked it sends its value out to its DataChannel, which is the same channel that it is monitoring. When an option button receives a value from its DataChannel, if the value is equivalent to its own value, it sets its checked state to true and repaints itself.

Can you see how it would be modified to be a non-exclusive choice control? Certainly the boundary condition of an OptionButton with no one else in the group would get halfway there. But recall that the way the button gets unset is that some other choice is set. Obviously the mouseUp method would have to implement a toggle rather than a single set semantic. I'll leave it as an exercise for you to create this class (although there is a version in the sources), and when your write it, change the box shape from round to square. This will give users the idea that this is not a single-choice option.

Instruments of control

Of course the fact that at any time the collection of OptionButtons that share the same DataChannel contain the currently selected choice is useful, but we'd like them to affect the display (or at least I would as it motivates this next section).

Let us say, for the sake of argument, that by selecting an OptionButton you would like to change the appearance of some of the controls on the page. This is a straightforward modification to the OptionButton; it simply needs a control message to send on some channel that will be received by the listening control. Implementing this "simple" feature is a bit tougher on the receiving end.

Traffic jam

The first problem of receiving messages from multiple channels is differentiation between destinations. Recall that in our

temperature example

, our Numeric classes were listening to the SLIDER channel and displaying updates as they came in. If we send our message on the SLIDER channel not only does it go to one Numeric control but it goes to the other Numeric control and the Slider itself. Needless to say this inability to specify exactly which control should receive the message is problematic. The next obvious, and wrong, choice is to simply open another data channel and send our control message to that channel, however you will recall that the Numeric thread is blocked waiting for the value to come in on the channel where values are sent, and thus it won't wake up until a value had arrived.

There are many ways to address this problem. I chose to implement a new data channel called a DataChannelMux. This multiplexing channel can accept inputs from any number of data channels, package the results into a common object type (the MuxItem), and forward them on a single data channel to the control applet. As a MuxItem, the object contains information about which channel it comes from, and the control object need only do a simple switch statement to de-multiplex the channels on the receiving end. The key concept of the channel multiplexor is that a collection of threads work together to collect and route messages to the receiver. The MuxItem class is shown below.

 1 public class MuxItem {
 2    private String fromChannel;
 3    private int      userIndex;
 4    private Object channelValue;
 5    MuxItem(String c, int i, Object v) {
 6        super();
 7    fromChannel = c;
 8    userIndex = i;
 9    channelValue = v;
10    }
11    public Object value() { return channelValue; } 
12    public int index() { return userIndex; }
13    public String wasChannel() { return fromChannel; }
14 }

As you can see, this is mostly a data structure that encapsulates objects forwarded over a DataChannel. A MuxItem is created with attributes for the channel it was received from, the object that had been passed, and an index supplied from the user. The latter acts as an accelerator for processing the items, as we will see below.

Note also that Java does not provide any sort of "read-only" specifier. Thus, to keep the MuxItem from being corrupted by an inadvertent write to one of its instance variables, the class provides only accessor methods for its internal state, and the instance variables are all private.

So this is our data structure, and now we need to create the structure for combining data from several channels. This is the job of a the DataChannelMux, which is discussed next.

 1 public class DataChannelMux implements Runnable {

The class is runnable since a DataChannelMux runs its own collection thread.

 2    Vector scythes = null;

The scythes vector keeps track of the channels we're tracking, originally so that they could be "mowed down" when the mux shuts down. However, I've found it easier to simply release the channels individually.

 3    private Thread DCM_TID = null;
 4    private String DCM_Channel;
 5    private int DCM_Index;
 6    private DataChannel DCM_Input;
 7    private DataChannel DCM_Output;

Then there is this collection of private variables that defines how the mux will work: the thread ID, the channel name that the mux delivers muxed messages to, the user index, and the actual data channels themselves.

 8    public DataChannelMux(String inChans[], String outChan) {
 9    super();
10    DCM_Output = DataChannel.getChannel(outChan);
11    scythes = new Vector();
12    for (int i = 0; i < inChans.length; i++) {
13        DataChannelMux dcm = new DataChannelMux(inChans[i], i, 
14        scythes.addElement(dcm);
15        dcm.start();
16    } 
17    }
18    DataChannelMux(String chanName, int userIndex, DataChannel 
outChan) {
19    super();
20    DCM_Channel = chanName;
21    DCM_Index = userIndex;
22    DCM_TID = new Thread(this);
23    DCM_Input = DataChannel.getChannel(chanName, DCM_TID, 8);
24    DCM_Output = outChan;
25    }

Then there are two constructors. Like DataChannels, the DataChannelMux class has two forms of instantiation. In the first constructor in lines 8 through 17, an array of DataChannel names are passed in inChans[], and the named channels are multiplexed onto a single output DataChannel named outChan. The output DataChannel is created for writing, and the second constructor is used to construct a DataChannelMux object that will run as a thread to monitor messages from a single DataChannel, passing them on to the output channel. The second constructor builds this object and stores housekeeping information in its private variables, and is ready to run.

Additionally there is a method to add channels after the DataChannelMux object has been created. This method, addChannel, uses the fact that the reference to scythes is null in monitoring versions of the mux object to prevent adding channels to a monitoring object rather than to the base object.

26    public boolean addChannel(String chanName, int userIndex) {
27  if (scythes == null)
28      return false; // if a monitor, fail the add
29  DataChannelMux dcm = new DataChannelMux(chanName, userIndex, 
30  scythes.addElement(dcm);
31  dcm.start();
32  return true;
33   }

When the new monitoring object is added, it immediately begins receiving data on the DataChannel.

The start method for the mux is simply :

34    public void start() { DCM_TID.start(); }

which calls start on the thread, which calls run(). And in the run() method it is simply :

35    public void run() {
36    if ((DCM_Output == null) || (DCM_Input == null)) 
37        return; // sanity check 
38    while (true) {
39        Object x = null;
40        try {
41      x = DCM_Input.getValue();
42        } catch (DataChannelOverrun e1) {
43      System.out.println("MUX overrun.");
44        } catch (DataChannelShutdown e2) {
45      return; // time to exit.
46        } catch (DataChannelException e3) { } // ignore others
47        MuxItem m = new MuxItem(DCM_Channel, DCM_Index, x);
48        DCM_Output.putValue(m);
49    }
50    }     
51 }

As you can see, this method waits for a value on the DataChannel this object is monitoring, and then puts it into a MuxItem and delivers it to the single output channel that the client is monitoring.

Putting the mux to work

Thus, the basic idea of the mux channel is that various controls can write to different channels, and other controls can collect the output of several channels simultaneously. To demonstrate how this works, we add a feature to the OptionButton class to send control messages to one or more data channels when they are selected, and enhance the Numeric control to allow various aspects of their appearance and operation to be changed by messages they receive.

The modified OptionButton class parses parameters from the HTML file of the form:

<param name=msgXX value="channel:message">

These parameters are numbered sequentially from 0 as msg0, msg1, and so on. In the value field we put a two-valued field, which consists of a channel name, a colon, and a string to send to that channel when selected. These are parsed with a method as follows:

 1    OptionMessage parseOption(String op) {
 2       String chan = op.substring(0, op.indexOf(':'));
 3       String msg = op.substring(op.indexOf(':')+1);
 4       return new OptionMessage(chan, msg);
 5    }

And the OptionMessage class is simply:

 1 class OptionMessage {
 2      String msg;
 3      DataChannel dc;
 4      public OptionMessage(String c, String m) {
 5        dc = DataChannel.getChannel(c);
 6        msg = m;
 7      }
 8      void send() {
 9        dc.putValue(msg);
10      }
11 }

Here it keeps the message to send and the channel to send it to, and whenever we call send() on an OptionMessage object it does just that.

Hello, option for you

Of course, sending messages is only half the story; the other half is dealing with them. The Numeric class was heavily enhanced to support dynamic formatting. While it is too long to reproduce entirely here, the complete source is available and free for your use (with one caveat: the source is "article quality," not "production quality"). I'll touch on the major changes, and the comments in the source should illuminate the minor changes.

If you recall in the previous column we used HTML around the applet to provide the caption indicating what the numeric value represented. The first change was to create a parameter for the applet named "label," which holds the default label for the applet. The applet renders the label in the Helvetica font to the left of the numeric display. The display is right-aligned in the applet container so that stacking several applets will allow them to line up easily. I also added a format parameter that allowed fixed-point numbers to be represented in the applet. The syntax for format is "M,Y" where M is the number of digits in the whole part and Y is the number of digits in the fractional part of the display.

The second major change was to accept a parameter for a control DataChannel. When present the startup of Numeric is modified as follows:

1 2 Page 1
Page 1 of 2