throw new LockedAccountException ; }
} _decrementTimerOn = false;
resetCounter ; return;
} doesnt need to be fully synchronized. In particular, the method
wrapperAround -
GetClientHost , which is simply a wrapper around a threadsafe static method in
RemoteServer , doesnt need to be synchronized. However, at this point, were reaching
diminishing returns; theres a certain value in simply having a few core methods that are entirely synchronized.
Remember, these last few rewrites are valid only if client computers send a single request at a time. For example,
checkAccess isnt nearly sophisticated enough to
differentiate between two clients running at the same IP address. If we need to distinguish between two such clients,
the client will probably have to pass in a unique identifier well actually do this when we discuss testing in
Chapt er 13
. In general, reducing the number of synchronization blocks often involves making assumptions about client
behavior. Its a good idea to document those assumptions.
12.2.3.2 Dont synchronize across device accesses
There are times, however, when no matter how small the method or how crucial the data in it, you should still avoid synchronizing the method. The most common situation is when the method
accesses a physical device with uncertain latency. For example, writing a log file to a hard drive is a very bad thing to do within a synchronized method. The reason for this is simple. Suppose
were in a scenario with many instances of
Account , and they all log their transactions to a log
file,
[ 3]
as shown in Figur e 12- 1
.
[ 3]
Or to a database. The discussion applies equally to a database.
Figure 12-1. Using a singleton logger
A simple version of this is quite easy to implement: package com.ora.rmibook.chapter12;
import java.io.; public interface Logger {
public void setOutputStreamPrintStream outputStream; public void logStringString string;
} package com.ora.rmibook.chapter12;
import java.io.; public class SimpleLogger implements Logger {
private static SimpleLogger _singleton; public synchronized static SimpleLogger getSingleton {
if null==_singleton { _singleton = new SimpleLogger ;
} return _singleton;
} private PrintStream _loggingStrea m;
private SimpleLogger { _loggingStream = System.err;
a good default value }
public void setOutputStreamPrintStream outputStream { _loggingStream = outputStream;
} public synchronized void logStringString string {
_loggingStream.printlnstring; }
} Note that the static method
getSingleton must be synchronized. Otherwise, more than
one instance of SimpleLogger
can be created. Aside from its rather pathetic lack of functionality™
SimpleLogger takes strings and sends them
to a log file, which is not the worlds most sophisticated logging mechanism™there is a significant flaw in this object: the account servers must all wait for a single lock, which is held for indefinite
amounts of time. This adds a bottleneck to the entire server application. If the hard disk slows down or is momentarily inaccessible e.g., the log file is mounted across the network, the entire
application grinds to a halt.
In general, the reasons for using threading in the first place imply that having a single lock that all the threads acquire fairly often is a bad idea. In the case of logs or database connections, it may
be unavoidable. However, in those cases, we really need to make the time a thread holds a lock as short as possible.
One common solution is to wrap the actual logging mechanism in an outer shell, which also implements the
Logger interface, as shown in
Figur e 12- 2 .
Figure 12-2. An improved singleton logger
The way this works is that a piece of code calls logString
. The wrapper class takes the string that is supposed to be logged and simply places it in a container, after which, the method
returns. This is a very fast operation. Meanwhile, the background thread executes the following loop:
1. Sleep for awhile. 2. Wake up and get all the available strings from the wrapper class.
3. Send the strings to the real logging mechanism. 4. Return to step 1.
The code for this logger isnt all that complicated either. In the following example, weve implemented the background thread as an inner class:
public class ThreadedLogger implements Logger { private SimpleLogger _actualLogger;
private ArrayList _logQueue; public ThreadedLoggerSimpleLogger actualLogger {
_logQueue = new ArrayList ; new BackgroundThread.start ;
} public void setOutputStreamPrintStream outputStream {
_actualLogger.setOutputStreamoutp utStream; }
public synchronized void logStringString string { _logQueue.addstring;
} private synchronized Collection getAndReplaceQueue {
ArrayList returnValue = _logQueue; _logQueue = new ArrayList ;
return returnValue; }
private class BackgroundThread extends Thread { public void run {
whiletrue { pause ;
logEntries ;
} }
private void pause { try {
Thread.sleep5000; }
catch Exception ignored{} }
private void logEntries { Collection entries = getAndReplaceQueue ;
Iterator i = entries.iterator ; whilei.hasNext {
String nextString = String i.next ; _actualLogger.logStringnextString;
} }
} }
We still have the problem in which different account servers may want to write to the log at the same time, and an instance of
Account may be forced to wait. Weve simply replaced a
potentially slow and high-variance bottleneck with a fast and low-variance bottleneck. This may still be unacceptable for some. If instances of
Account are constantly accessing the
log, though each individual logString
operation is quick, each instance of Account
still waits for many other instances to execute
logString . For example, suppose that an
average of 30 instances of Account
are waiting to use the log file. Then the logString
method is really 30 times slower than it appears to be at first glance. Think about that sentence again. What I just asserted is this: the real time it takes to log a string
is approximately the time it takes to log a string multiplied by the number of accounts waiting to log a string. However, suppose we have a burst of activity. The number of accounts waiting to log
strings may increase, which means that every instance of Account
becomes slower. This is pretty nasty; our system will slow down by more than a linear factor during burst periods.
How nasty is this? Run a profiler and find out. Thats really the only way to tell why a program isnt performing well.
If this is a problem, and we need to eliminate the bottleneck entirely, a variant of the preceding technique often works. Namely, we make the following three changes:
• Each server has a separate log object into which it stores logging information.
• Each of these logs registers with the background thread.
• The background thread now visits each of the logs in sequence, getting the entries and
sending them to the real logging mechanism. This effectively means that accounts cannot interfere with each other. Each instance of
Account writes to a distinct logging object. The instance of
Account may be forced to wait momentarily
while the background thread gets the entries from its log. However, that wait is bounded and small. See
Figur e 12- 3 .
Figure 12-3. A more complex logging example
This approach to logging and other forms of device access is often called using a batch thread. Our logging code is remarkably similar to our implementation of automatic lock maintenance
using a background thread. You may start to see a pattern here.
12.2.3.3 Dont synchronize across secondary remote method invocations