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, 
DCM_Output);
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, 
DCM_Output);
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    public void start() {
 2        updater = new Thread(this);
 3        localChannel = DataChannel.getChannel(idName, updater, 8);
 4        if (controlName != null) {
 5            chans = new String[2];
 6            chans[0] = channelName;
 7            chans[1] = controlName;
 8        } else {
 9            chans = new String[1];
10            chans[0] = channelName;
11        }
12        myChannel = new DataChannelMux(chans, idName);
13        updater.start();
14    }

Now instead of simply opening the single DataChannel, if the control channel is specified, it is opened as well, and the instance variable myChannel gets a DataChannelMux rather than a simple DataChannel. Further, we add a new channel, localChannel, that has the output DataChannel from the mux.

Once this is done we modify the run method of Numeric to appear as follows:

 1    public void run() {
 2      String ctl;
 3      double x = 0;
 4      MuxItem z;
 5      while (Thread.currentThread() == updater) {
 6          try {
 7              z = (MuxItem)(localChannel.getValue());
 8              switch (z.index()) {
 9                  case 0:
10                      x = ((Integer)(z.value())).doubleValue();
11                      setValue(x);
12                      break;
13                  case 1:
14                      parseControl((String)(z.value()));
15                      break;
16              }
17          } catch (DataChannelOverrun e) {
18              System.out.println("Numeric: OVERRUN!");
19          } catch (DataChannelShutdown e) {
20              System.out.println("Numeric thread shutting 
down.");
21              return;
22          } catch (DataChannelTimeout e) {
23              continue;
24          } catch (DataChannelException ee) {
25              System.out.println("We're dead.");
26              ee.printStackTrace();
27          }
28          repaint();
29        }
30    }

As you can see in line 7, what comes from localChannel is now a MuxItem rather than an Integer object. The MuxItem will have the array index of the channel from which the object originated. Since we create them so channel 0 is always the channel that values are sent to and channel 1 is the channel that control messages are sent to, a simple case statement is all that is needed to demultiplex the messages on our side.

If the object came from channel 0, it is treated identically as in the previous column. The currentValue is changed and the applet repaints itself. If, however, the object comes from channel 1, the message is a control message and is parsed by a private method called parseControl.

This method defines the syntax of control messages accepted by the Numeric control and is defined below.

 1   void parseControl(String ctl) {
 2        if (ctl.startsWith("minvalue:")) {
 3            try {
 4              minValue = 
Double.valueOf(ctl.substring(9)).doubleValue();
 5              setValue(0.0);
 6              return;
 7            } catch (NumberFormatException e) { return; }
 8        } else if (ctl.startsWith("maxvalue:")) {
 9          try {
10              maxValue = 
Double.valueOf(ctl.substring(9)).doubleValue();
11              setValue(0.0);
12              return;
13            } catch (NumberFormatException e) { return; }
14        } else if (ctl.startsWith("label:")) {
15            myLabel = ctl.substring(6);
16        }
17        return;
18    }

Control messages have the form "tag:value" and are parsed with the string method startsWith. The three tags that are accepted by the Numeric class are minvalue:, maxvalue:, and label:. When each of these tags are received, they change the respective value in the Numeric control and cause the applet to repaint.

This encapsulation, leaving it up to the Numeric control to parse its controls, allows complete freedom in the control message protocol between controls. Any control that implements a control channel need only describe its syntax for other users to send appropriate messages to the control applet.

Putting it all together

The results of this work mean that our simple temperature converter page can now be expanded into a general-purpose converter of values. Some of the HTML would be as follows:

<applet code="Numeric.class" width=300 height=30>
<param name=datachannel value=slider>
<param name=id value=num1>
<param name=controlchannel value=control1>
<param name=format value="5,3">
<param name=label value="Number 1">
<param name=bgcolor value=#f0f000>
</applet>

This would put a Numeric control on the page with a default label of "Number 1" that was listening to the DataChannel "slider" for updates to its value. Further it listens to DataChannel "control1" for requests to change its appearance. Finally it displays its output in a 5.3 format or 99999.000 type numbers, and the background color in this case is sort of a dark yellow.

We can control this applet from our page with some option buttons that were laid out with this HTML:

<applet code="OptionButton.class" width=200 height=30>
<param name=bgcolor value="#f0f000">
<param name=group value=units>
<param name=id value=1>
<param name=msg0 value="control1:label:Celsius">
<param name=msg1 value="control1:minvalue:0">
<param name=msg2 value="control1:maxvalue:100">
<param name=label value="Celsius/Fahrenheit">
<param name=datachannel value=control>
</applet><br>
<applet code="OptionButton.class" width=200 height=30>
<param name=bgcolor value="#f0f000">
<param name=group value=units>
<param name=id value=2>
<param name=msg0 value="control1:label:Meters">
<param name=msg1 value="control1:minvalue:0">
<param name=msg2 value="control1:maxvalue:100">
<param name=label value="Meters/Feet">
<param name=datachannel value=control>
</applet><br>
<applet code="OptionButton.class" width=200 height=30>
<param name=bgcolor value="#f0f000">
<param name=group value=units>
<param name=id value=3>
<param name=msg0 value="control1:label:Kilometers">
<param name=msg1 value="control1:minvalue:0">
<param name=msg2 value="control1:maxvalue:50">
<param name=label value="Kilometers/Miles">
<param name=datachannel value=control>
</applet>

Here you can see we have three option buttons that are mutually exclusive because they all listen to the Datachannel "units" (specified by the "group" parameter). When any one of them is pressed, they set the minvalue, maxvalue, and label of the Numeric applet by sending control messages to the "control1" DataChannel.

Wrapping it up

This brings to a close the chapter on using threads in Java to accomplish old tricks in new ways. If you have followed the entire series, you can see how threads enable complex interactions to occur in ways that are easier to write and understand. But you should also keep in mind that threads are simply another tool you can add to your toolbox when you write in Java. A good way to think about them is to put them in the same category as recursion. With the introduction of locally scoped variables and "infinite" call stacks into the programming mainstream, it became possible to write very elegant and very clever algorithms that involved methods calling themselves with additional or different parameters. (Recursive descent compilers are the best example of a good use for recursion.) Java is the first programming language that demands that threads be available in all implementations. It is this guarantee that makes it possible to assume threads and the benefits they offer. However, there is also a downside to threads.

New technologies in languages are typically poorly supported by the host operating system at first. When recursion was introduced and then implemented on machines that primarily ran non-recursive languages like Fortran and BASIC, there were times when it just didn't work reliably. (Anyone remember the 4-kilobyte stack page on the PDP 11?) This is true of threads today.

For threads to be implemented reliably and with good performance, the host operating system (if it exists) needs to understand threads. This understanding is expressed in the form of system libraries that are re-entrant, and device drivers that do not block entire processes when they wait for I/O to occur. These assumptions have been particularly difficult to meet on the Macintosh and "free" Unix-derived operating systems. Sun has promised to provide clear and unambiguous requirements for the Java threading model in the Java specification. I expect the public support of the maintainers of these operating systems will continue to enhance the robustness of Java running on those platforms. However, for the next few months you may experience some difficulties if you use a lot of threads in your programs. Such is the life of the pioneer, first to see the prairie, first to discover rattlesnakes.

Chuck McManis is currently the director of system software at FreeGate Corp. FreeGate is a venture-funded start-up that is exploring opportunities in the Internet marketplace. Before joining FreeGate, McManis was a member of the Java group. He joined the Java group just after the formation of FirstPerson Inc. and was a member of the portable OS group (the group responsible for the OS portion of Java). Later, when FirstPerson was dissolved, he stayed with the group through the development of the alpha and beta versions of the Java platform. He created the first "all Java" home page on the Internet when he did the programming for the Java version of the Sun home page in May 1995. He also developed a cryptographic library for Java and versions of the Java class loader that could screen classes based on digital signatures. Before joining FirstPerson, Chuck worked in the operating systems area of SunSoft developing networking applications, where he did the initial design of NIS+.
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more