Q: I would like to know how to write a thread-safe client/server program in Java. How should I approach the threading? What sort of issues should I be aware of?
A: Thread-safe programming is only necessary if you have data that can be modified by more than one thread at a time. In a client/server situation, usually the server has multiple clients. If all those clients do is read data from the server, and if no other programs are modifying that data, then you have nothing to worry about. But if those clients can change shared data on the server, they must not be allowed to conflict with one another. Normally, if two clients try to change shared data, you have to hope that the first client is able to finish with the data before the second client begins to modify it. This situation is called racing.
Under these circumstances, the outcome depends on how the underlying scheduler happens to allocate time to the various clients' requests. In the case of Java, one doesn't know what the underlying scheduler's policies are (because they're platform-dependent, or natively implemented), but even with a known scheduling policy, the actual allocation will depend on the availability of various resources (typically I/O) and user inputs; therefore it is never readily predictable.
Thread-safe design replaces racing situations with choreographed access to shared data. In Java, thread-safe design gets a lot of support from the underlying language (through synchronized blocks) and the standard class library (through the wait/notify mechanism in
Object). The simplest way to make a thread-safe design is to use synchronized blocks so that only one client can access the server at any one time. All other clients must wait until the server finishes with that client. Obviously, such a solution doesn't scale well as more clients are added, since clients will have to wait a long time for their turn to access the server.
In fact, if a poorly implemented server entered an infinite loop during a call by one client, then all its other clients would wait forever. This is an example of a general problem called starvation, which has to do with situations where a client never finishes its task because one or more other clients have monopolized the resource it needs.
In general, a client shouldn't monopolize a resource that it isn't actively using. When a client has a resource but then blocks while waiting for another resource (typically for I/O), Java will allow a second client to access the server. This situation makes sense, and is often desirable, but must be taken into account in the choreography since the first client may not actually have been done with its task, and the second client may want to access the same resources. The way around this is for the server design to use
notify() calls within the synchronized blocks, so that the various clients know when it's safe to proceed.
If resource A on the server is independent of resource B, it is reasonable for the server to allow one client to use A at the same time that it allows another client to use B (rather than having the second client wait for the first one to finish with A before it can use B).
This situation is safe, as long as A and B are really independent and given than they remain independent after subsequent changes to the server. If A and B aren't truly independent, we can end up with what is called deadlock, a situation in which the first client has A and also needs B, while the second client has B and also needs A. This is a special case of starvation, in which the client shares responsibility for its own starvation. Note that Oaks and Wong state in Java Threads that "Deadlock ... is the hardest problem to solve in any threaded program."
In summation, thread-safe programming seeks to maximize efficiency by eliminating racing situations, while at the same time avoiding starvation situations.
Learn more about this topic
- Java Threads, Second Edition, Scott Oaks, Henry Wong, et al. (O'Reilly, 1999) http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=1565924185