Java Design:
Building Better Apps and Applets

Chapter 4: Design with Threads

This document is a mirror page!

This file is a ROUGH conversion of a JAVA DESIGN pre-production file into HTML, using HoTmeTaL Pro.

JAVA DESIGN includes over 170 illustrations. None are included here (HoTMeTaL Pro does not automatically convert .wmf artwork).

So these pages will give you a good idea about what the book is all about, yet is no substitute for the real thing (thank goodness!).

Title: JAVA DESIGN: Building Better Apps and Applets
Authors: Peter Coad and Mark Mayfield
ISBN: 0-13-271149-4

Back to Table of Contents
On to Chapter 5

Design with Threads

This chapter is about concurrency-doing more than one thing at a time.

About Threads

What

A thread is a single stream of program execution: one statement after the next after the next.

Figure4-1. A single thread, winding its way through some objects

Multiple threads are more than one stream of program execution. The program runs one statement after the next after the next-and then switches to a different part of the program and runs one statement after the next after the next-and then switches to a different part of the program and runs one statement after the next after the next-and so on.

Figure 4-2. Two threads, winding their way through some objects

Multiple threads might follow the same path (Figure 4-2) or different paths (Figure 4-3), as they wind their way through a program.

Figure 4-3. Two threads, each winding its own way through some objects

How do threads get started?

How do threads get started? How many threads am I working with?

One way: a user interface (UI) starts a thread. Each client has its own thread. So with multiple clients, you have multiple threads; your design must account for this.

Another way: thread objects start a thread. Thread objects, you say? Yes. In Java, there is a class called Thread. You can ask the class to create a new thread object for you. Then you can tell that thread object to start-and (along with other threads) that thread will begin (at whatever starting point you give it) and wind its way through objects in your app.

Cool, huh?

So you can add thread objects, too. When? When you need additional program streams, above and beyond just a programming stream for each client. Why? If you need to give the appearance of doing more than one thing at a time, thread objects are the tools you need to get the job done. For example, at Zoe's Zones, we could run monitoring at high level of priority, yet let assessing of reliability run a at a lower level of priority.

Figure 4-. Two threads beginning from UI objects, one thread beginning from a thread object

It's helpful to think of a thread object an object, when commanding a thread to start, stop, and the like.

It's helpful to visualize a thread as something that starts from a client or thread object, and then winds its way through your application.

Why use multiple threads?

Most designs must account for multiple streams of program execution; this chapter shows how to do that, safely.

Multiple threads let you give the appearance of doing more than one thing at a time. For example, your application can serve multiple clients at the same time.

Threads also give you the clean, simple way to design-in the main thing you want your application to do, along with other things that you'd like it to be aware of or check on from time-to-time (like a background calculation or some other mundane chore).

Threads simplify a design when you need to give the appearance that you are doing more than one thing at a time. And threads improve the timeliness, the response time, when a higher-priority part of an application needs to run.

Figure 4-. Without threads, designing-in the appearance that you are doing more than one thing at a time gets complicated in a hurry!

Yet if you don't need them, don't use them.

If you don't need to give the appearance of doing more than one thing at a time, don't use multiple threads.

Why?

First and foremost: keep your design simpler. One thing at a time. No conflicts between shared resources. All is well.

Second: keep your overhead down. Multiple threads add processing overhead, the time it takes to change from one thread to the next; it's called context switch time. Your app eats up some microseconds each time it switches from one thread to another. It adds up.

Sync

In Java, a sync'd method is a synchronized method. It's synchronized in this way:

same object, immediately entering into that method without delay

- a thread within a sync'd method can invoke sync methods another object;

yet if another thread is already inside, it must wait in a queue just like

any other thread.

Sync: a guarantee and a non-guarantee

A sync guarantees that only one thread will run within a method for an object.

A sync has a non-guarantee when it comes to good service, though: any other threads that come along to invoke sync'd methods within that object wait in a queue, standing by until the first thread exits the sync'd method.

Figure 4-. Other threads waiting to invoke sync'd methods within that object must wait till the first thread leaves the sync'd method it is executing

Other threads, running in other objects, even running in the same object (winding their way through non-sync'd methods) continue to run-and get turns running, perhaps even before the thread that was running in a sync'd method gets the opportunity to complete ending that sync.

Hence, a sync does not guarantee that a thread will run to completion before being interrupted by another thread, somewhere else in the application.

Sync: scope

You can sync an instance method-so just one thread at a time can enter and work with the values of its instance variables.

You can sync a class method-so just one thread at a time can enter and work with the values of its class variables.

Footnote: In Java, you can even sync a block of code within a method. A better idea: put that code in a separate method, instead. Reason why: you'll end up with smaller methods (a good idea), more cohesive methods (a good idea), harder-to-miss sync's (also a good idea).

Footnote: An object with one or more sync'd methods has a lock. When a thread enters one of its sync'd method, the lock for that object is set (no other thread can enter that sync'd method or any other sync'd method for that object ). When a thread exits a sync'd method, the lock for that object is reset. (Similarly, for a class with one or more sync'd class methods.)

Shared value (and keeping out of trouble)

Threads and tasks (real-time processes) are very similar. Okay, then: in what few ways are they different? Threads share the same set of values in a running program; each task has its own set of values.

Figure 4-. Threads share the same internal values

Consequently, the overhead for threads is lower; newer operating systems all support threads; it's the trendy way to support concurrency.

Also consequently, as designers, we have to make sure that multiple threads don't work with the same values at the same time. Otherwise, we might get unexpected results.

Figure 4-. Why methods that work with internal values must be synched

In the figure:

- Thread 1 executes the first part of the statement:

if 4 < 5

- Thread 2 gets a turn and executes the first part of the same statement:

if 4 < 5

- Thread 1 executes the second part of the statement:

5 = 4 + 1

- Thread 2 get a turn and executes the second part of the same statement:

6 = 5+1

And so count ends up with a value greater than max-not at all what we expected.

How do we get around this problem?

Methods that work with internal values must be synched (sometimes referred to as "locked"), meaning that such methods should allow just one thread in at a time.

Synchs add processing overhead, so it's not a good idea to sync everything in sight.

Yet keep in mind that if you don't sync up something that you should, values can become corrupted, leading to erroneous results.

Checking your design for "thread safeness" is an important aspect of designing with multiple threads.

"Sync Access to Values" Strategy: When multiple threads compete for values(s) within an object-and you try other thread paths yet cannot avoid competition for those values-use sync'd methods to limit access (one thread at a time). For multithreaded objects, sync each method that compares, operates on, gets, or sets internal values.

Don't sync longer than you have to.

The idea in a multithreaded design is to let multiple threads run through your application.

When you sync a method, and a thread enters that sync, then no other thread can enter any sync'd method in that object until that sync method runs to completion. It's similar to a traffic jam which squeezes down to one lane: if traffic is light, no problem; it traffic is heavy, the queue stretches out for miles and miles.

This means that it's a good idea to streamline a sync'd method, including just those steps that must be sync'd.

"Zoom In and Sync" Strategy: Zoom in on exactly what you need to sync, factor it out into a separate method, and sync that method. Why: sync for as little time as possible, so other (potentially higher-priority) threads waiting at the start of other sync methods for that object will get to run sooner, rather than later.

Shared resource (and keeping out of trouble)

Too many syncs can hang your application.

Watch out whenever a sync'd method extends its reach to other methods. If that thread hits another sync'd method in some other object, and another thread is inside, then it must wait-and might be stuck forever.. Welcome to the wonderful world of deadlock.

Figure4-. Deadlock

Here's how it happens:

Thread 1 enters a sync'd method. Then, before exiting that sync'd method, follows a method call outside of that object.

Thread 2 enters a sync'd method. Then, before exiting that sync'd method, follows a method call outside of that object.

Thread 1 arrives at the sync'd method that Thread 2 already entered-and waits for Thread 2 to exit that method.

Thread 2 arrives at the sync'd method that Thread 1 already entered-and waits for Thread 1 to exit that method.

You can find deadlock in everyday life-although usually someone gives in and allows it to end.

Figure 4-. One day at the mall: each car sync'd one space, then waited at each other's sync'd space (with some drivers, this deadlock could last forever).

Sounds like a pair of strategies is needed here, analogous to "when to sync access to values" and "how to sync access to values."

This time, it's at a bit higher level of abstraction.

"Sync Access to Objects" Strategy: When multiple threads compete for entry into each others sync'd methods, use a gatekeeper to control access, one thread at a time; and make sure the objects that the gatekeeper protects have no sync methods.

What can you do about deadlock like this? Add an object that acts as a gatekeeper; design the object interactions so that each thread first sync's on a gatekeeper's method, followed by exclusive sync'd access to the objects that the gatekeeper protects.

In a parking lot, you can add a gatekeeper by: (1) adding a very stern parking attendant or (2) slanting the parking slots to encourage one-way travel down the rows in the parking lot (one-way streets in large cities have a similar effect).

Multiple Clients, Multiple Threads within an Object

Back to Charlie's Charters and its reservation system.

As soon as the reservation has more than one agent making reservations at the same time, then we have multiple clients and multiple threads.

Here it's not a matter of whether or not to use multiple threads; multiple threads are definitely part of the design.

Here, it's a matter of using multiple threads safely. Hmmm. It sounds like it is time for a couple of strategies:

"Value Gatekeeper" Strategy: Look for a method that increments or decrements a count of a limited resource. Sync that method; give it exclusive access to that count.

"Object Gatekeeper" Strategy: Look for a method that reserves or issues a limited resource, represented by the objects in that collection. Sync that method; give that method exclusive access to that collection of objects.

Apply these strategies to Sandpiper.

What is the limited resource are we working with? Space on a scheduled flight.

Do we manage a counter or a collection of objects? If we had a seat map and made seat assignments, then we'd have a collection of objects. Yet at Sandpiper, all we have is a counter, the count of the number of reservations on the flight.

So, based upon the "value gatekeeper" strategy, we'd expect to sync a method within the ScheduledFlight class.

Here's what the object model look like:

Figure 4-. Multiple threads could lead to inadvertent overbooking-need to sync here

Sync the method responsible for comparing the current number of reservations against the capacity, and adding in the new reservation. It's the "try to add reservation" method.

Here's the scenario view:

Figure 4. Making a reservation: two clients, two threads

In Java, it looks like this:

public class ScheduledFlight extends Object {

#

// attributes / private / object connections

private Vector reservations = new Vector();

// methods / public / conducting business

public synchronized

Reservation tryToAddReservation(Date aDate, Passenger aPassenger) {

// code goes here

if (this.hasRoom)

// code goes here

this.addReservation(aDate, aPassenger)

// code goes here }

// methods / private protected / conducting business

private protected boolean hasRoom() {

// code goes here }

private protected Reservation addReservation (Date aDate, Passenger aPassenger) {

// code goes here }

#

}

Code notes: We limit the visibility of hasRoom and addReservation and only call them within a synchronized method. If at some point we need to add a reservation without first having to check if there's room, then we will synchronized addReservation. The reservations vector is the resource that we need to protect.

Multiple Thread Objects, Multiple Threads within an Object

Now let's take a look at Zoe's Zones.

We're working with zones and sensors. This time, we're beginning with just one thread, and considering when and if we might add additional threads. Here is a helpful strategy:

"Four Thread Designs" Strategy: Apply these thread designs, looking for the simplest one that will satisfy your performance requirements. From simplest to most complex, consider: #1 single thread, #2 prioritized-object threads, #3 prioritized-method threads, #4 prioritized-method prioritized-object threads.

#1 - Single thread

The simplest solution is a single-thread solution.

It's not high-tech sexy. After all, threads and concurrency are really fun things to mess around with. Yet wait a minute. We're not in a classroom. We're not considering a group of dining philosophers who like sharing their eating utensils with each other . We're designing an application.

A simpler design is a better design-if that simpler design gets the job done within time, budget, and resource constraints.

Could a single-thread solution work here? Let's take a closer look.

Figure 4-. A single-thread solution

Someone creates a thread object and asks it to start. At that point, the thread becomes runnable. When that runnable thread gets a turn to run, the thread object tells its corresponding zone object to run-and so the thread begins winding its way through the scenario.

In Java, it looks like this:

public class Zone extends Object implements Runnable {

#

// attributes / private / thread

private Thread mainThread;

// attributes / private / object connections

private Vector sensors = new Vector();

// methods / public / activation

public void activate() {

// create the main thread and start it

this.mainThread = new Thread(this);

this.mainThread.start(); }

// methods / public / Runnable implementation

public void run() {

for(;;) { // loop forever until thread is stopped

this.monitor(); } }

// methods / public / conducting business

public void monitor() {

// iterate through the vector of sensors and tell each sensor to readAndCheck

Enumeration sensorList = this.sensors.elements();

while (sensorList.hasMoreElements()) {

// must cast the element to a Sensor

Sensor aSensor = (Sensor)activatableList.nextElement();

aSensor.readAndCheck(); } }

#

}

Code notes: This is just the snippet for Zone. To kick things off we have an activate method that creates the main thread and tells it to start. The run method has an internal loop that will continue to call monitor until the main thread is stopped. The monitor method iterates through the vector of sensors, telling each one to read and check.

A single thread winds its way through each zone and its sensors. For each sensor, the thread reads a sensor, compares the reading with a threshold, logs any detected problems, assesses sensor reliability.

Just one thread runs through the objects. No one to fight with; nothing to fight over; no sync's needed.

This simplest design will work, if the thread can run around fast enough and get everything done on time.

Yet what if you have hundreds of zones and thousands of sensors? A single thread might still be okay.

You must take how long it takes to run a single thread through every zone and its sensors, and then compare it with the sampling rate which is required. If the processing time is 10 minutes (a very long time) and the sampling rate is once per hour, then a single-thread design will get the job done.

Not elegant. Not high-tech cool. Yet a cost-effective, more easily-implemented solution. Come to think of it, that is high-tech cool.

#2 - Prioritized-object threads

What if a single-thread solution is just not fast enough? Then what?

Mindlessly adding threads with the hope of things running faster would be rather futile. Threads add overhead-context switch time, sync and end-sync times-and might slow down your app.

Multiple threads will save you time, make the application appear to run faster, if and only if some threads can run at a higher priority than other threads.

One approach: add threads within higher-prioritized objects.

What are the priority objects in this system?

Zone-low

Some sensors-high

Some sensors-medium.

So use three prioritized-object threads.

This is a multithreaded solution, with multiple threads running through the same object. Sensor status is the attribute that both threads use.

In a multithreaded design, look for where threads intersect. Here, the threads intersect at the status of a sensor.

Let's use the "zoom in and sync" strategy, moving in as closely as we can. Rather than sync these large methods:

sync readAndCheckValue

sync monitor,

let's sync as close as possible to the intersection of these threads, a pair of methods that act as "value gatekeepers"-namely:

sync getStatus

sync setStatus.

Figure 4-. A trio of prioritized-object threads

In Java, it looks like this:

public class Zone extends Object implements Runnable {

#

// attributes / private / thread

private Thread myThread;

// attributes / private / object connections

private Vector sensors = new Vector();

// methods / public / activation

public void activate(int priority) {

// create my thread and start it

this.myThread = new Thread(this);

this.myThread.setPriority(priority);

this.myThread.start(); }

// methods / public / Runnable implementation

public void run() {

for(;;) { // loop forever until thread is stopped

this.monitor(); } }

// methods / public / conducting business

public void monitor() {

// iterate through the vector of sensors and tell each sensor to checkStatus

Enumeration sensorList = this.sensors.elements();

while (sensorList.hasMoreElements()) {

// must cast the element to a Sensor

Sensor aSensor = (Sensor)activatableList.nextElement();

String status = aSensor.checkStatus();

// evaluate status} }

#

}

Code notes: This time, the activate method takes a parameter to set the priority of the thread. Also, since each sensor performs the readAndCheck on its own thread, we simply ask each sensor for its status.

public class Sensor extends Object implements Runnable {

#

// attributes / private / thread

private Thread myThread;

// methods / public / activation

public void activate(int priority) {

// create my thread and start it

this.myThread = new Thread(this);

this.myThread.setPriority(priority);

this.myThread.start(); }

// methods / public / Runnable implementation

public void run() {

for(;;) { // loop forever until thread is stopped

this.readAndCheck(); } }

// methods / public / conducting business

public String checkStatus() {

// code goes here }

// methods / private protected synchronized / conducting business

private protected synchronized String getStatus() { // code goes here }

private protected synchronized void setStatus(String status) {

// code goes here }

// methods / private protected / conducting business

private protected void readAndCheck() {

// code goes here

// calls the following methods:

// readValue, checkValue, logProblem, and assessReliability }

private protected int readValue() { // code goes here }

private protected int checkValue(int value, Range aRange) { // code goes here }

private protected void logProblem() { // code goes here }

private protected void assessReliability() { // code goes here }

#

}

Code notes: This time, Sensor needs its own activate method an priority parameter, just like Zone. We limited the visibility of most of the major conducting- business methods. The only main public method, other than activate and run, is the checkStatus method, invoked by a zone. The getStatus and setStatus methods must be synchronized.

That looks just fine. Yet there is an added problem that you need to consider, whenever you have different priorities for threads with the same basic responsibilities; it's called "starvation."

Starvation occurs when a low-priority thread never gets a turn; this happens when higher-priority threads keep the processor always busy.

Starvation is not a problem, if the amount of processing time is small compared to the total time available.

As processing time approaches total time available, the likelihood of starvation of one or more threads is more likely. If timely servicing of low-priority threads is not a big deal, then all is well.

In Zoe's case, starvation will cause zones to be starved of their monitoring behavior first, followed by the starvation of both monitoring and accessing behavior of the lower priority sensors. Not a good thing for Zoe's or her customers.

If timely servicing of low-priority threads is important, though, then we've got two options.

Option 1. Add a thread manager. A thread manager can tell all high-priority threads to go to sleep for a short period of time, making sure that low-priority threads get executed along the way. It's like adding a socialized government to make sure everyone gets their fair share.

Option 2. Don't use prioritized-object threads; we're merely applying a single-thread solution on a sensor-by-sensor basis; indeed, a single-thread solution would be simpler and faster.

#3 - Prioritized-method threads

We've tried single threads. We've tried prioritized-object threads.

Yet if high-priority methods within some objects are still not getting executed in a timely fashion, we can take a look at another prioritization scheme: give execution priority to those methods.

Here's how to do it:

"Prioritized Methods" Strategy: Prioritize your methods. Separate-out cohesive functions with different priorities. Run higher-priority methods in higher-priority threads; run lower-priority methods in lower-priority threads.

The higher-priority thread, now relieved of doing lower-priority work, will run in less time. So in places where a single-thread approach fails (for not being fast enough), prioritized-method threads can save the day.

The cost? Some added design complexity. Required. And well worth it.

What are the method priorities in this system?

Monitor zone-low

Assess sensor reliability-medium

Monitor sensor-high

What if the sampling rate for reading sensors is so fast at times, that we just cannot keep up?

The key words here are "at times." After all, if we are forever too slow, time-averaging some of the work will be of no help at all.

If we cannot keep up at times, we can spin off lower-priority work into a separate thread (or threads)-and then let the app catch up on that work after a while.

For sensor monitoring, we could strip it down to "read and save a sample" and then let another thread take care of "checking and logging a problem report, if any."

We'll pursue that design in Chapter 5.

For now, let's stick with three prioritized-method threads: monitor zone, assess sensor reliability, and monitor sensor.

This is a another multithreaded solution, with multiple threads running through the same object. Sensor status is the attribute that both threads use.

Figure 4-. A trio of prioritized-method threads

In Java, it looks like this:

public class Sensor extends Object implements Runnable {

#

// attributes / private / threads

private Thread monitorThread;

private Thread assessingThread;

// methods / public / activation

public void activate(int monitoringPriority, int assessingPriority) {

// create monitoring thread and start it

this.monitoringThread = new Thread(this);

this.monitoringThread.setPriority(monitoringPriority);

this.monitoringThread.start();

// create assessing thread and start it

this.assessingThread = new Thread(this);

this.assessingThread.setPriority(assessingPriority);

this.assessingThread.start(); }

// methods / public / Runnable implementation

public void run() {

// if the current thread entering run is the monitoring thread,

// then read and check until stopped.

if (Thread.currentThread == this.monitoringThread ) {

for(;;) { // loop forever until thread is stopped

this.readAndCheck(); } }

// else if the current thread entering run is the assessing thread,

// then assess reliability until stopped

if (Thread.currentThread == this.assessingThread ) {

for(;;) { // loop forever until thread is stopped

this.assessReliability(); } } }

// methods / public / conducting business

public String checkStatus() {

// code goes here }

// methods / private protected synchronized / conducting business

private protected synchronized String getStatus() { // code goes here }

private protected synchronized void setStatus(String status) {

// code goes here }

// methods / private protected / conducting business

private protected void readAndCheck() {

// code goes here

// calls the following methods:

// readValue, checkValue, and logProblem. }

private protected int readValue() { // code goes here }

private protected int checkValue(int value, Range aRange) { // code goes here }

private protected void logProblem() { // code goes here }

private protected void assessReliability() { // code goes here }

#

}

Code notes: The Zone remains the same. Sensor's activate method now takes two parameters: one for the monitoring thread priority and one for the assessing thread priority. The run method checks the current thread and performs the appropriate loop. This time, the readAndCheck method does not call the assessReliability method because assessing has its own thread.

#4 - Prioritized-method prioritized-object threads

We've tried single threads. We've tried prioritized-method threads. We've tried prioritized-object threads.

Yet if the simpler approaches won't get the job done, if we need maximum prioritization flexibility, then we can prioritize methods and prioritize objects.

If we have some methods that are more important than others, and if we have some objects that are more important than others, and if processing time is approaching actual time, then this approach is one to carefully consider.

Based upon prioritized methods, what threads do we need, and what are the relative priorities? Monitor sensor, across all sensors-high

Assess sensor reliability, across all sensors-medium

Monitor zone, across all zones-low.

Based upon prioritized objects, what threads do we need, and what are the relative priorities?

High-priority sensors-high

Low-priority sensors-medium

Zone-low.

Merging these lists, we get five kinds of threads in this design::

Monitor a high-priority sensor-very high

Monitor a low-priority sensor-high

Assess a high-priority sensor-medium

Assess a low-priority sensor-low

Monitor a zone-very low.

Notice that monitoring lower-priority sensors is still at a higher level of priority than assessing the reliability of the higher-priority sensors. This way, all monitoring runs at a higher level of priority than any assessing.

Hence, the overall thread count for this design works out to:

One thread per zone object

Two threads per high-priority or low-priority sensor.

That's a lot, it's true. This is an extreme case. The good thing about this design: it's got room for a lot of fine-tuning and tweaking. The bad thing about this design: it uses lots and lots of threads-not a good thing, unless it must be done to meet system response time requirements for high-priority sensors. Hence, this strategy:

"Thread Count" Strategy. Justify the existence of each thread in your design. If you can reduce the thread count and meet response time requirements, do so.

A scenario view follows, representing six threads, two zones, two priorities of sensors, two thread priorities within each sensor, and a partridge in a pear tree.

Figure 4-. Prioritized methods, prioritized objects-and lots and lots of threads

In Java, it looks like this:

Code notes: It's the same souse code as in the previous example. When creating sensors, adjust the priority parameters in the activate method so that high-priority sensors have a higher monitoring priority than low-priority sensors. Make sure that the assessing priority for the high-priority sensors is lower than the monitoring priority for the low-priority sensors, or low-priority sensors may not have enough processing time to perform their monitoring. For example:

// set monitoring priority to 8 and assessing priority to 3

Sensor aSensorHi = new Sensor(8,3);

// set monitoring priority to 7 and assessing priority to 2

Sensor aSensorLo = new Sensor(7,2);

This approach allows the greatest flexibility, lots of room for fine-tuning.

Overall point

The overall point here is: keep it simple.

Use a single-thread design (approach #1) whenever you can.

If you cannot get the job done that way, then you've got to prioritize what you do. Identify priority methods, adding sub-methods to separate-out cohesive functions with different priorities. Give priority to certain objects (approach #2). Give priority to certain methods (approach #3). Or give priority to both..

Interface Adapters

"Designing with threads" is an advanced design topic. "Designing with interface adapters" is even more advanced. So, depending upon your background in this area, you might opt to jump ahead to the summary at the end of this Chapter-and tackle this section another day.

Let's explore the wonderful world of interface adapters.

The need

A thread object is rather single-minded in what is sends to another object. A thread object sends a "run" message. That's it.

So what happens is multiple thread objects want to message the same object-and yet invoke different methods for each one?

One approach: dispatcher

One approach: add a test and a case statement to the run method.

Figure 4-. Threads wanting to invoke different methods, yet being forced through one entry point

The test statement asks a thread class which thread has just entered. The case statement routes the thread to the method it really needs to be running through.

Yet a "case" statement at the beginning of any method implies some pretty lame internal cohesion.

A thread should be able to invoke the method it needs. If multiple threads want to invoke different methods in the same object, though, we need to add an intermediary: an interface adapter.

A better approach: interface adapters

Yes, why not let each thread invoke the method it really wants to invoke, rather just a "run" method?

Yet how can we do this, when a thread implements the Runnable interface (run) rather than an application-specific message?

The answer is: use an interface adapter.

Figure 4-. An interface adapter adapts interfaces

A sender tells an adapter to do something; that adapter translates the method call in one interface to another interface. Hence, the adapter is called an "interface adapter."

What an interface adapter looks like

Here's an object-model pattern, the interface-adapter pattern:

Figure 4-. The interface-adapter pattern

Note that the interface names in that figure are not really interface names at all. They are abstractions of interface names that you might use, when applying this object-model pattern in a specific context.

The specific interfaces you choose to use will be different for each kind of interface-adapter class that you define for your application. For Zoe's Zones, we'll use this pattern in that context to model something called, "thread-Runnable-to-IMonitor adapter-sensors." Before we do that, though, let's take a look at the scenario view for this pattern, so we can consider its stereotypical interactions.

A receiver:

Then a sender sends a message to its "IX-to-IY" adapter. And that interface adapter send a message to its object that implements the "IY" interface.

It all looks like this:

Figure 4-. Stereotypical interactions for interface-adapter pattern

In Java, it looks like this:

public interface IX {

void x(); }

public interface IY {

void y(); }

public class Sender extends Object {

#

// attributes / private / object connection

private IX myIX;

// methods / public / conducting business

public void xInvoker() {

// send the message x to myIX

this.myIX.x(); }

// constructors

public Sender(IX anIX) {

// set myIX to anIX

this.myIX = anIX; }

#

}

public class IX_to_IY_Adapter extends Object implements IX {

#

// attributes / private / object connection

private IY myIY;

// methods / public / IX implementation

public void x () {

// send the message y to myIY

this.myIY.y(); }

// constructors

public Sender(IY anIY) {

// set myIY to anIY

this.myIY = anIY; }

#

}

public class IY_Implementer extends Object implements IY {

#

// attributes / private / object connections

private IX_to_IY_Adapter myAdapter;

private Sender mySender;

// methods / public / adapter creation

public void createAdapter () {

// create an adapter and pass myself as the parameter

this.myAdapter = new IX_to_IY_Adapter(this);

// create a sender and pass myAdapter as the parameter

this.mySender = new Sender(this.myAdapter); }

// methods / public / IY implementation

public void y() { // code goes here }

#

}

When should you use interface adapters for threads? Only when we need them. When do you need them? In the context of working with thread objects: whenever you have multiple thread objects that you would like to invoke different methods in an object. And, in a broader context: whenever you want to adapt a message-send from one object into some other message-send, suitable for another object.

Interface adapters for Zoe's zones

So here is what happens for Zoe's Zones-for the "prioritized methods" approach.

We need threads to wind through each:

a zone-monitoring thread

a sensor-monitoring thread

a sensor-assessing thread.

A zone-monitoring thread

A zone-monitoring thread needs no interface adapter. Why? Because there is only one method to service, the "monitor" method.

The scenario view looks like this:

Figure 4-. One thread, winding its way through each zone and its sensors

Note that the Zone class implements the Runnable interface, so a thread object can send a zone object a "run" message.

A sensor-assessing thread and a sensor-monitoring thread

Okay. Let's get ready for the next scenario view.

Now we can apply the interface-adapter pattern. The pattern, applied in this context, looks like this:

Figure 4-. Applying the interface-adapter pattern within a specific context

Hmmm. We need a class that will implement the IMonitor interface. An object in that class will know all of the sensor objects in the application.

Note that normally, we could embed this "across the collection" responsibility within the Sensor class itself (making sure that, upon creating a sensor objects, we add it to that collection).

Yet here, we are dealing with interfaces. And (Java-style) interfaces are intended for object method signatures, rather than class method signatures.

The impact in a scenario view? Not all that much. We just add a "Sensors" column, a class with one object in it, an object that holds a collection of all the sensors in the application.

What other classes do we need here? The MonitorAdapter class implements the simple mapping between "run" and "monitor." The AssessAdapter class implements the simple mapping between "run" and "assess."

For the scenario itself, here's what it takes just to get things ready to go:

At this point, we're ready for business:

And:

Here is the scenario view:

Figure 4-. Monitoring and assessing threads-with corresponding interface adapters

In Java, it looks like this:

public interface IMonitor {

void monitor(); }

public interface IAssess {

void assess(); }

public class RunnableToIMonitorAdapter extends Object implements Runnable {

#

// attributes / private / object connection

private IMonitor myIMonitor;

// methods / public / Runnable implementation

public void run() {

this.myIMonitor.monitor(); }

// constructors

public RunnableToIMonitorAdapter(IMonitor anIMonitor) {

this.myIMonitor = anIMonitor ; }

#

}

public class RunnableToIAssessAdapter extends Object implements Runnable {

#

// attributes / private / object connection

private IAssess myIAssess;

// methods / public / Runnable implementation

public void run() {

this.myIAssess.assess(); }

// constructors

public RunnableToIAssessAdapter(IAssess anIAssess) {

this.myIAssess = anIAssess ; }

#

}

public class Sensor extends Object implements IMonitor, IAssess {

#

// attributes / private / threads

private Thread monitorThread;

private Thread assessingThread;

// attributes / private / interface adapters

private RunnableToIMonitorAdapter myRunnableToIMonitorAdapter;

private RunnableToIAssessAdapter myRunnableToIAssessAdapter;

// methods / public / activation

public void activate(int monitoringPriority, int assessingPriority) {

// create the monitoring adapter and pass myself as the parameter

myRunnableToIMonitorAdapter = new RunnableToIMonitorAdapter(this);

// create monitoring thread and pass the monitoring adapter as the parameter

this.monitoringThread = new Thread(myRunnableToIMonitorAdapter);

this.monitoringThread.setPriority(monitoringPriority);

this.monitoringThread.start();

// create the assessing adapter and pass myself as the parameter

myRunnableToIAssessAdapter = new RunnableToIAssessAdapter(this);

// create monitoring thread and pass the assessing adapter as the parameter

this.assessingThread = new Thread(myRunnableToIAssessAdapter);

this.assessingThread.setPriority(assessingPriority);

this.assessingThread.start(); }

// methods / public / IMonitor implementation

public void monitor() {

for(;;) { // loop forever until thread is stopped

this.readAndCheck(); } }

// methods / public / IAssess implementation

public void assess() {

for(;;) { // loop forever until thread is stopped

this.assessReliability(); } }

// methods / public / conducting business

public String checkStatus() {

// code goes here }

// methods / private protected synchronized / conducting business

private protected synchronized String getStatus() { // code goes here }

private protected synchronized void setStatus(String status) {

// code goes here }

// methods / private protected / conducting business

private protected void readAndCheck() {

// code goes here

// calls the following methods:

// readValue, checkValue, and logProblem. }

private protected int readValue() { // code goes here }

private protected int checkValue(int value, Range aRange) { // code goes here }

private protected void logProblem() { // code goes here }

private protected void assessReliability() { // code goes here }

#

}

Code notes: The Zone remains the same. Sensor now implements the interfaces for two interface adapters, and no longer implements the Runnable interface. The monitoring thread now enters through the IMonitor interface and the assessing thread now enters through the IAssess interface.

Footnote: Java is strongly typed. All possible message-sends must be established at compile time. You cannot design with dynamic message dispatching [meaning, there is no mechanism like in C++ to: create a pointer to a function and then de-reference that pointer (invoke the method it points to); and at any time reassign that pointer and then deference invoke that pointer (invoke the method it points to now)].

Summary

In this chapter, you've worked with threads, streams of program execution.

For an application to run, you must have at least one thread.

Why bother with multiple threads?

Most designs must account for multiple streams of program execution; this chapter shows how to do that, safely.

Multiple threads let you give the appearance of doing more than one thing at a time. For example, your application can serve multiple clients at the same time.

Threads also give you the clean, simple way to design-in the main thing you want your application to do, along with other things that you'd like it to be aware of or check on from time-to-time.

The strategies you learned and applied in this chapter are:

"Sync Access to Values" Strategy: When multiple threads compete for values(s) within an object-and you try other thread paths yet design-out such competition-use sync'd methods to limit access (one thread at a time). For multithreaded objects, sync each method that compares, operates on, gets, or sets internal values.

"Zoom In and Sync" Strategy: Zoom in on exactly what you need to sync, factor it out into a separate method, and sync that method. Why: sync for as little time as possible, so other (potentially higher-priority) threads waiting at the start of other sync methods for that object will get to run sooner, rather than later.

"Sync Access to Objects" Strategy: When multiple threads compete for entry into each others sync'd methods-and you try other thread paths yet cannot design-out such competition-use a gatekeeper to control access, one thread at a time; and make sure the objects that the gatekeeper protects have no sync methods.

"Value Gatekeeper" Strategy: Look for a method that increments or decrements a count of a limited resource. Sync that method; give it exclusive access to that count.

"Object Gatekeeper" Strategy: Look for a method that reserves or issues a limited resource, represented by the objects in that collection. Sync that method; give that method exclusive access to that collection of objects.

"Four Thread Designs" Strategy: Apply these thread designs, looking for the simplest one that will satisfy your performance requirements. From simplest to most complex, consider: #1 single thread, #2 prioritized-object threads, #3 prioritized-method threads, #4 prioritized-method prioritized-object threads.

"Prioritized Methods" Strategy: Prioritize your methods. Separate-out cohesive functions with different priorities. Run higher-priority methods in higher-priority threads; run lower-priority methods in lower-priority threads.

"Thread Count" Strategy. Justify the existence of each thread in your design. If you can reduce the thread count and meet response time requirements, do so.

cover Back to internet programming Links (ip-Links) page
Back to Java OO Design & Coding Standards (joodcs) page
Back to "Java Design" book page
Back to Table of Contents
Back to Chapter 3
Source http://www.oi.com/4.htm
On to Chapter 5

Home | Search | What's New | Workshops | Books
Software | Support | Free Goodies | Newsletters | Feedback