Section 10.5
Threads and Network Programming
NETWORK PROGRAMS ARE a natural application for threads.
Threads were discussed in Section 7.6
in the context of GUI programming. (If you have not already read that
section, it would be a good idea to do it now.) As we saw in that section,
a thread could be used in a GUI program
to perform a long computation in parallel with the event-handling thread
of the GUI. Network programs with graphical user interfaces can use
the same technique: If a separate thread is used for network communication,
then the communication can proceed in parallel with other things that
are going on in the program. Threads are even more important in server
programs. In many cases, a client can remain connected to a server
for an indefinite period of time. It's not a good idea to make other
potential clients wait during this period. A multi-threaded
server starts a new thread for each client. Several threads can
run at the same time, so several clients can be served at the same time.
The second client doesn't have to wait until the server is finished
with the first client. It's like a post office that opens up a new window
for each customer, instead of making them all wait in line at one window.
Now, there are at least two problems with the command-line chat examples,
CLChatClient and CLChatServer, from the
previous section.
For one thing, after a user enters a message, the user must
wait for a reply from the other side of the connection. It would
be nice if the user could just keep typing lines and see the other
user's messages as they arrive. It's not easy to do this in
a command-line interface, but it's a natural application
for a graphical user interface. The second problem has to do
with opening connections in the first place. I can only run
CLChatClient if I know that there is a CLChatServer
running on some particular computer. Except in rather contrived
situations, there is no way for me to know that. It would be
nice if I could find out, somehow, who's out there waiting for
a connection. In this section, we'll address both of these
problems and, at the same time, learn a little more about
network programming and about threads.
To address the first problem with the command-line chat programs,
let's consider a GUI chat program. When one user connects
to another user, a window should open on the screen with an
input box where the user can enter messages to be transmitted
to user on the other end of the connection. The user should be able to send a message
at any time. The program should also be prepared to receive
messages from the other side at any time, and those messages
have to be displayed to the user as they arrive. In case this
is not clear to you, here is an applet that simulates such a program.
Enter a message in the input box at the bottom of the applet,
and press return (or, equivalently, click the "Send" button):
Both incoming messages and messages that you send are posted to
the JTextArea that occupies most of the applet.
This is not a real network connection. When you send your
first message, a separate thread is started by the applet.
This thread simulates incoming messages from the other side
of a network connection. In fact, it just chooses the messages
at random from a pre-set list. At the same time, you can continue
to enter and send messages. The run() method
that is executed by the thread carries out the following
algorithm:
Post the message "Hey, hello there! Nice to chat with you."
while(running):
Wait a random time, between 2 and 12 seconds
Select a random message from the list
Post the selected message in the JTextArea
The variable running is set to false when the applet
is stopped, as a signal to the thread that it should exit. The thread
is created and started in the actionPerformed method that responds
when you press return or click the "Send" button for the first time.
You can find the complete source code in the file
ChatSimulation.java,
but I really want to look at the programming for the real thing rather
than the simulation. The GUI chat program that we will look at is
ChatWindow.java. The interface
in this program will look similar to the simulation, but there will
be a real network connection, and the incoming messages will be coming
from the other side of that connection. The basic idea is not much
more complicated than the simulation. A separate thread is created
to wait for incoming messages and post them as they arrive. The
run() method for this thread has an outline that is similar
to the one for the simulation:
while the connection is open:
Wait for a message to arrive from the other side
Post the message in the JTextArea
However, the whole thing is complicated by the problem of opening
and closing the connection and by the input/output errors that can
occur at any time. The ChatWindow class is fairly sophisticated,
and I don't want to cover everything that it does, but I will describe
some of its functions. You should read the source code
if you want to understand it completely.
First, there is the question of how a connection can be
established between two ChatWindows. As the
ChatWindow class is designed, the connection must
be established before the window is opened. Recall that
one end of a network connection is represented by on object
of type Socket. The connected Socket
is passed as a parameter to the ChatWindow constructor.
This makes ChatWindow
into a nicely reusable class that can be used in a variety of programs
that set up the connection in different ways. The
simplest approach to establishing the connection uses a command-line interface,
just as is done with the CLChat programs. Once the connection
has been established, a ChatWindow is opened on each side of the
connection, and the actual chatting takes place through the windows
instead of the command line. For this
example, I've written a main() routine that can act as either the server
or the client, depending on the command line argument that it is given.
If the first command line argument is "-s", the program will act as a server.
Otherwise, it assumes that the first argument specifies the computer where
the server is running, and it acts as a client. The code for doing this is:
try {
if (args[0].equalsIgnoreCase("-s")) {
// Act as a server. Wait for a connection.
ServerSocket listener = new ServerSocket(port);
System.out.println("Listening on port "
+ listener.getLocalPort());
connection = listener.accept();
listener.close();
}
else {
// Act as a client. Request a connection with
// a server running on the computer specified in args[0].
connection = new Socket(args[0],port);
}
out = new PrintWriter(connection.getOutputStream());
out.println(HANDSHAKE);
out.flush();
in = new TextReader(connection.getInputStream());
message = in.getln();
if (! message.equals(HANDSHAKE) ) {
throw new IOException(
"Connected program is not a ChatWindow");
}
System.out.println("Connected.");
}
catch (Exception e) {
System.out.println("Error opening connection.");
System.out.println(e.toString());
return;
}
ChatWindow w; // The window for this end of the connection.
w = new ChatWindow("ChatWindow", connection);
As it happens, I've taken the rather twisty approach of putting this
main() routine in the ChatWindow class itself. (Possibly,
it would be better style to put the main() routine in a different class.)
This means that you can run ChatWindow as a standalone program.
If you run it with the command "java ChatWindow -s", it will run
as a server. To run it as a client, use the command "java ChatWindow <server>",
where <server> is the name or IP number of the computer where the server is
running. Use "localhost" as the name of the server, if you want to test the program
by connecting to a server running on the same computer as the client.
Whether the program is running as a client or as a server,
once a connection is made, the window will open,
and you can start chatting.
The constructor for the ChatWindow has the job of starting a thread
to handle incoming messages. It also creates input and output streams for
sending and receiving. The part of the constructor that performs these tasks
look like this (with just a few changes for the sake of simplicity):
try {
incoming = new TextReader( connection.getInputStream() );
outgoing = new PrintWriter( connection.getOutputStream() );
// Here, connection is the Socket that will be used for
// communication. Input and output streams are created
// for writing and reading information over the connection.
}
catch (IOException e) {
// An error occurred while trying to get the streams.
// Set up user interface to reflect the error. The
// "transcript" is the JTextArea where messages are displayed.
transcript.setText("Error opening I/O streams!\n"
+ "Connection can't be used.\n"
+ "You can close the window now.\n");
sendButton.setEnabled(false);
connection = null;
}
/* Create the thread for reading data from the connection,
unless an error just occurred. */
if (connection != null) {
// Create a thread to execute the run() method in this
// applet class, and start the thread. The run() method
// will wait for incoming messages and post them to the
// transcript when they are received.
reader = new Thread(this);
reader.start();
}
The input stream, incoming, is used by the thread to read
messages from the other side of the connection. It does this simply
by saying incoming.getln(). This command will not return until
a line of text has been received or until an error occurs. The
output stream, outgoing, is used by the actionPerformed()
method to transmit the text from the text input box.
When either user closes his ChatWindow, the connection
must be closed on both sides. The connection might also be closed
because an error occurs, such as a network failure. It takes some care
to handle all this correctly. Take a look at the source code
if you are interested.
There is still a big problem with running ChatWindow
in the way I've just described. Suppose I want to set up a connection.
How do I know who has a ChatWindow running as a server?
If I start up the server myself, how will anyone know about it?
The CLChat programs have the same problem. What I would
like is a program that would show me a list of all the "chatters"
who are available, and I would like to be able to add myself to
the list so that other people can tell that I am available to
receive connections. The problem is, who is going to keep the
list and how will my program get a copy of the list?
This is a natural application for a server! We can have a server
whose job is to keep a list of available chatters. This server can
be run as a daemon on a "well-known computer", so that it is always
available at a known location on the Internet. Then, a program anywhere on
the Internet can contact the server to get the list of chatters or to
register a person on the list. That program acts as a client for the
server.
In fact, I've written such a server program.
It's called ConnectionBroker, and the source code is
available in the file ConnectionBroker.java.
The main() routine of this server is similar to the main() routine
of the DateServe example that was given at the beginning of
this section. That is, it runs in an infinite loop in which it accepts
connections and processes them. In this case, however, the
processing of each request is much more complicated and can take
a long time, so the main()
routine sets up a separate thread to process each connection request.
That's all the main routine does with the connection.
The thread takes care of all the details, while the main program goes on
to the next connection request. Here is the main() routine from
ConnectionBroker:
public static void main(String[] args) {
// The main() routine creates a listening socket and
// listens for requests. When a request is received,
// a thread is created to service it.
int port; // Port on which server listens.
ServerSocket listener;
Socket client;
if (args.length == 0)
port = DEFAULT_PORT;
else {
try {
port = Integer.parseInt(args[0]);
}
catch (NumberFormatException e) {
System.out.println(args[0] + " is not a legal port number.");
return;
}
}
try {
listener = new ServerSocket(port);
}
catch (IOException e) {
System.out.println("Can't start server.");
System.out.println(e.toString());
return;
}
System.out.println("Listening on port " + listener.getLocalPort());
try {
while (true) {
client = listener.accept(); // Get a connection request.
new ClientThread(client); // Start a thread to handle it.
}
}
catch (Exception e) {
System.out.println("Server shut down unexpectedly.");
System.out.println(e.toString());
System.exit(1);
}
}
Once the processing thread has been started to handle the connection,
the thread reads a command from the client, and carries out that command.
It understands three types of commands:
- A REGISTER command that adds the client to the list of
available chatters. The server keeps this list in an internal
data structure. The connection remains open and the thread waits
for some other user to request a connection with that client. Once a connection
is made, the client is removed from the list.
- A SEND_CLIENT_LIST command requests a copy of the list
of available chatters. The server responds by sending the list and
closing the connection.
- A CONNECT command requests the server to set up
a connection with one of the chatters in the list. The server sets
up the connection, and -- if no error occurs -- informs both
parties that a connection has been established. The two parties
can then start sending messages to each other. (These messages
actually continue to pass through the server. The direct network
connections are between the server and the two clients. The server
relays messages from each client to the other. It's done this
way so that a ConnectionBroker will work with applets as clients,
as long as the applets are loaded from the computer where the server
is running. An applet is not ordinarily allowed to make network connections,
except to the computer from which it was loaded.)
To use a ConnectionBroker, you need a program that
acts as a client for the ConnectionBroker service. I have
an applet that does this. The applet tries to connect to a
ConnectionBroker server on the computer from which the
applet was loaded. If no such server is running on that computer,
the applet will display an error notification saying that
it can't connect to the server. You are likely to get an error
message unless you have downloaded this on-line textbook and are reading
the copy on your own computer. In that case, you should be able to run the
ConnectionBroker server on your computer and use the
applet to connect to it. (Just compile ConnectionBroker.java and then
give the command "java ConnectionBroker" in the same directory.
It will print out "Listening on port 3030" and start waiting for
connections. You will have to abort the program in some way to
get it to end, such as by hitting CONTROL-C.) Here is the applet:
If the applet does find a server, it will display the list of
available chatters in the JComboBox on the third line of the applet.
If no chatters are available on the server, then
you'll just see the message "(None available)". Once you register
yourself, you will be included in this list, and you can open
a connection to yourself. (Not a very interesting conversation
perhaps, but it will demonstrate how the program works.)
The procedures for registering yourself with the server and for
requesting a connection to someone in the JComboBox should
be easy enough to figure out. When you register yourself,
a ChatWindow will open and will wait for someone to
connect to you. A ChatWindow will also open when you
request a connection.
You can enter yourself multiple times in the list, if you want,
and you can connect to multiple people on the list. A separate
ChatWindow will open for each connection.
This networked
chat application is still very much a demonstration system. That
is, it is not robust enough or full-featured enough to be used
for serious applications. I tried to keep the interactions among the
server, the applet, and the connection windows simple enough to understand
with a reasonable effort. If you are interested in pursuing the topic
of network programming, I suggest you start by reading the
three source code files for this example: the
applet BrokeredChat.java,
the server ConnectionBroker.java,
and the window ChatWindow.java.
The Problem of Synchronization
Although I don't want to say too much about the ConnectionBroker
program, there is still one general question I want to look at: What happens
when two or more threads use the same data? When this is the case, it's possible
for the data to become corrupted, unless access to the data is carefully
synchronized. The problem arises when two
threads both try to access the data at the same time, or when one thread
is interrupted by another when it is in the middle of accessing the data.
Synchronization is used to make sure that this doesn't happen. To see what
can go wrong, let's look at a typical example: a bank account. Suppose that
the amount of money in a bank account is represented by the class:
public class BankAccount {
private double balance; // amount of money in account
public double getBalance() {
return balance;
}
public void withdraw(double amount) {
// Precondition: The balance is >= the amount.
balance = balance - amount;
}
.
. // Other methods
.
}
Suppose that account is an object of type BankAccount, and that
this variable is used by several threads. Suppose that one of these threads
wants to do a withdrawal of $100. This should be easy:
if ( account.getBalance() >= 100)
account.withdraw();
But suppose that two threads try to do a withdrawal at the same time from an account
that contains $150. It might happen that one thread calls account.getBalance() and
gets a balance of 150. But at that moment, the first thread
is interrupted by the other thread. The other thread calls account.getBalance()
and also gets 150 as the balance. Both threads decide its safe to withdraw $100, but when
they do so, the balance drops below zero. Actually, its even worse than this.
The statement "balance = balance - amount" is actually
executed as several steps: Read the balance; subtract the amount; store the new balance.
It's possible for a thread to be interrupted in the middle of this. Suppose that
two threads try to withdraw $100. If they execute the withdrawal at about the same
time, it might happen that the order of operations is:
1. First thread reads the balance, and gets $150
2. Second thread reads the balance, and gets $150
3. Second thread subtracts $100 from $150, leaving $50
4. Second thread stores the new balance, $50
5. First thread (continuing after interruption)
subtracts $100 from $150, leaving $50
6. First thread stores the new balance, $50
The net result is that even though there have been two withdrawals of $100, the
amount in the account has only gone down by one hundred. The bank will probably not
be very happy with its programmers!
You might not think that this sequence of events is very likely, but when large
numbers of computations are being performed by several threads on shared data, problems
like this are almost certain to occur, and they can be disastrous when they happen.
The synchronization problem is very real: Access to shared data must be controlled.
As I mentioned in Section 7.6, the Swing GUI library
solves the synchronization problem in a straightforward way: Only one thread is allowed
to change the data used by Swing components. That thread is the event-handling thread.
If the some other thread wants to do something with a Swing component, it's not allowed
to do it itself. It must arrange for the event-handling thread to do it instead.
Swing has methods SwingUtilities.invokeLater() and SwingUtilities.invokeAndWait()
to make this possible. This is the only type of synchronization that is used in
the ChatSimulation, ChatWindow, and BrokeredChat programs.
In many cases, Swing's solution to the synchronization problem is not applicable
and might even defeat the purpose of using multiple threads in the first place.
Java has a more general means for controlling access to shared data. It's done
using a new type of statement: the synchronized statement.
A synchronized statement has the form:
synchronized ( <object-reference> ) {
<statements>
}
For example:
synchronized(account) {
if ( account.getBalance() >= amount )
balance = balance - amount;
}
The idea is that the <object-reference> -- account in the example --
is used to "lock" access to the statements. Each object in Java has a lock that can be used
for synchronization. When a thread executes synchronized(account), it takes possession
of account's lock, and will hold that lock until it is done executing the statements
inside the synchronized statement. If a second thread tries to execute
synchronized(account) while the first thread holds the lock, the second thread
will have to wait until the first thread releases the lock. This means that it's impossible
for two different threads to execute the statements in the synchronized statement
at the same time. The scenarios that we looked at above, which could corrupt the data, are
impossible.
It's possible to use the same object in two different synchronized statements.
Only one of those statements can be executed at any given time, because all the
statements require the same lock before they can be executed. By putting every access to some
data inside synchronized statements, and using the same object for synchronization
in each statement, we can make sure that that data will only be accessed by one thread
at a time. This is the general approach for solving the synchronization problem. It is
an approach that will work for multi-threaded servers, such as ConnectionBroker,
where there are many threads that might need access to the same data. The
ConnectionBroker program, for example, keeps a list of clients in a Vector
named clientList. This vector is used by many threads, and access to it
must be controlled. This is accomplished by putting all access to the vector in
synchronized statements. The vector itself is used as the synchronization
object (although there is no rule that says that the synchronization object has
to be the same as the data that is being protected). Here, for your amusement is
all the code from ConnectionBroker.java that accesses clientList:
/* These four methods synchronize access to a Vector, clientList,
which contains a list of the clients of this server. The
synchronization also protects the variable nextClientInfo. */
static void addClient(Client client) {
// Adds a new client to the clientList vector.
synchronized(clientList) {
client.ID = nextClientID++;
if (client.info.length() == 0)
client.info = "Anonymous" + client.ID;
clientList.addElement(client);
}
System.out.println("Added client " + client.ID + " " + client.info);
}
static void removeClient(Client client) {
// Removes the client from the clientList, if present.
synchronized(clientList) {
clientList.removeElement(client);
}
System.out.println("Removed client " + client.ID);
}
static Client getClient(int ID) {
// Removes client from the clientList vector, if it
// contains a client of the given ID. If so, the
// removed client is returned. Otherwise, null is returned.
synchronized(clientList) {
for (int i = 0; i < clientList.size(); i++) {
Client c = (Client)clientList.elementAt(i);
if (c.ID == ID) {
clientList.removeElementAt(i);
System.out.println("Removed client " + c.ID);
c.ID = 0; // Since this client is no longer waiting!
return c;
}
}
return null;
}
}
static Client[] getClients() {
// Returns an array of all the clients in the
// clientList. If there are none, null is returned.
synchronized(clientList) {
if (clientList.size() == 0)
return null;
Client[] clients = new Client[ clientList.size() ];
for (int i = 0; i < clientList.size(); i++)
clients[i] = (Client)clientList.elementAt(i);
return clients;
}
}
You don't have to understand exactly what is going on here, just that
the synchronized statements are used to control access to
data that is being shared by multiple threads. There is much more to learn about
threads; synchronization is only one of the problems that arise.
However, I will leave the topic here. (One reason why I covered this
much was to fulfill a promise made back in Section 3.6,
where there was a list of all the different types of statements in Java.
The synchronized statement was the last of these that we
needed to cover.)
End of Chapter 10