Concurrent Programming in Java
© 1996 Doug Lea

Assembly Line Example


This example illustrates the design and implementation of push-based flow systems via an assembly line applet that builds series of ``paintings'' in a style vaguely reminiscent of Piet Mondrian and Mark Rothko.

Representations

To start out, we need some base representation types. In this system, all elements can be defined as subclasses of abstract class Box, where every Box has a color and a size, can display itself when asked, and can be made to deeply clone (duplicate) itself. The color mechanics are default-implemented. Others are left abstract, defined differently in different subclasses:

Box.java

The overall theme of this example is to start off with sources that produce simple basic boxes, and then push them through stages that paint, join, flip, and embed them to form the paintings. BasicBoxes are the raw material:

BasicBox.java

Two fancier kinds of boxes can be made by joining side-by-side two existing boxes, adding a line-based border surrounding them. Joined boxes can also flip themselves. All this can be done in either of two ways, horizontally or vertically. The two resulting classes can be made subclasses of JoinedPair to allow sharing of some common code:

JoinedPair.java HorizontallyJoinedPair.java VerticallyJoinedPair.java

The final kind of fancy box wraps one Box within a border:

WrappedBox.java

Stages

Representations are now set up to support the following kinds of stages:

Two of these stages (Painters and Flippers) change the states of their sources using methods supported by the represented objects themselves, and then pass them on to other stages. Others accept zero (BasicBoxSource), one (Wrapper) or two (Joiners) incoming objects to create new kinds of Boxes. Both Cloners and Alternators are kinds of Splitters. Collectors and related stages come into play as utilities to help with some of the plumbing.

Interfaces

Looking ahead to how we might want to string these stages together, it is worthwhile to standardize on the interfaces. We'd like to be able to connect any stage to any other stage for which it could make sense, so we would like bland, non-committal names for the principal methods.

Since we are doing push-based flow, these interfaces mainly describe put-style methods. In fact, we could just call them all put, except that this doesn't work very well for Combiner stages. For example, a VerticalJoiner needs two put methods, one supplying the top Box, and one for the bottom Box. We could evade this by designing Joiners to take alternate incoming Boxes as the tops and bottoms, but this would make them harder to control. Instead, we'll use the somewhat ugly but easily extensible names putA, putB, and so on:

PushSource.java

PushStage.java

DualInputPushStage.java We can make the ``B'' channels of DualInputPushStages completely transparent to other stages by defining a simple Adapter class that accepts a putA but relays it to the intended recipient's putB. This way, most stages can be built to invoke putA, without knowing or caring that it is being fed into some successor's B channel:

DualInputAdapter.java

And, while we are focused on interfaces and adapters, here is a simple Runnable Adapter that helps perform any putA in a new Thread:

PutARunner.java

Sinks

Sinks have no successors. The simplest kind of sink doesn't even process its input, so serves as a way to throw away elements. In the spirit of Unix pipes and filters, we can call it:

DevNull.java

More interesting sinks require more interesting code. For example, in the applet used to produce the image shown at the beginning of this section, the Applet subclass itself was defined to implement PushStage. It served as the ultimate sink by displaying the assembled objects.

Connections

Interfaces standardize on the method names for stages, but do nothing about the linkages to successors, which will be maintained using some kind of instance variables in each stage object. Except for sinks such as DevNull, each stage has at least one successor. There are several implementation options, including:

The third option is simplest and works fine here. (In fact it is always a valid option. Stages with three or more outputs can be built by cascading those for only two. Of course, you wouldn't want to do this if most stages had large and/or variable numbers of successors.) This leads to base classes supporting either one or two links, and with one or two corresponding attachment methods, using a similar ugly suffix convention (attach1, attach2):

SimgleOutputPushStage.java

DualOutputPushStage.java

Now we can build all sorts of useful classes that extend either of these base classes, simultaneously implementing any of the standard interfaces.

Linear Stages

The simplest transformational stages are linear, single-input, single-output stages. Painters, Wrappers, and Flippers are just:

Painter.java

Wrapper.java

Flipper.java

The Painter and Wrapper stages apply to any kind of Box. But Flippers only make sense for JoinedPairs. If a Flipper receives something other than a JoinedPair, it will just pass it through. In a more ``strongly typed'' version, we might instead choose to drop boxes other than JoinedPairs by sending them to DevNull.

Dual Input Stages

The most basic kind of dual input stage is a simple Collector, that accepts messages on either channel, and relays them on to a single successor:

Collector.java We have two kinds of Combiners, horizontal and vertical Joiners. As did the representation classes, the stage classes share enough in common to build a common superclass. Joiner stages block further inputs until they are able to combine one item each from putA and putB. This can be implemented via the usual guard mechanics.

Joiner.java HorizontalJoiner.java VerticalJoiner.java

Dual Output Stages

Our multiple-output stages should generate threads to drive at least one of their outputs. This maintains liveness in cases where elements are ultimately passed to Combiners (here, the Joiners).

Alternators output alternate inputs on alternate channels.

Alternator.java

Cloners multicast the same element to both successors:

Cloner.java

A Screener is a stage that directs all inputs obeying some predicate to one channel, and all others to the other. We can build a generic one by encapsulating the BoxPredicate to check in an interface, and implementing it for example with a class that makes sure that a Box fits within a given (symmetric, in this case) bound. The Screener itself accepts a BoxPredicate and uses it to direct outputs:

BoxPredicate.java MaxSizePredicate.java Screener.java

Sources

Here is a sample source, one that produces BasicBoxes of random sizes. For convenience, it is also equipped with an autonomous loop repeatedly invoking start, interspersed with random production delays:

BasicBoxSource.java

Coordination

Without a scripting tool based on these classes, we have to program assembly lines by manually creating instances of desired stages, and linking them together. This is easy in principle, but tedious and error-prone in practice because of the lack of visual guidance about what stages are connected to what.

Here's a fragment of the flow used in the applet that produced the image displayed at the beginning of this section.

AssemblyLine.java


Doug Lea
Last modified: Tue Nov 4 10:13:10 EST 1997