| CPSC 329 | Software Development | Fall 2026 |
This lab provides practice with threads in a server: serving each client in its own thread, finding and fixing the problems that come from those threads sharing data, and shutting the server down politely.
Successful completion of this lab means that you:
Labs are for learning and practice — you may help and be helped by others when it comes to figuring out what to do, but in the end you need to understand and be able to do the things for yourself.
This lab can be done individually or in pairs. If you work together, use pair programming rather than splitting things up and working in parallel.
You may not use AI to figure out what to do or to do the task for you.
due Fri 11/13 at the start of class
Parts 1-3 are required. Part 4 is strongly encouraged, but not required for a complete handin.
To hand in your work:
If you worked with someone else, make sure both partners are named in writeup.txt. Only one needs to hand in the lab.
Copy your entire ~/cs329/dev/lab10 directory into your handin directory. Make sure you end up with lab10 (directly) inside your handin directory and that it contains a src directory (with your code) and a writeup.txt file (with your answers) — you shouldn't have a bunch of stuff loose in the top level of your handin directory or have folders nested deeper.
You have been provided with a server which poses riddles (and, as an optional extension, tells knock knock jokes) along with several clients.
Clients connect to the server and send commands, one per line. The server carries out each command and then waits for the next one until the client closes the connection. The commands:
| command | what happens |
|---|---|
| riddle | The server sends a riddle, the client sends its answer, the server replies either correct or incorrect, following incorrect with a second line containing the correct answer. |
| submit riddle | The client sends two more lines (a riddle and its answer). The server adds the riddle to its collection of riddles. |
| stats | The server replies with one line of statistics: how many clients it has served, how many riddles it has told, and how many riddles it has in its collection. |
| shutdown | Shut down the server. (Not implemented yet — that's part 4.) |
The files:
JokeServer — the server. main sets up the riddles and knock knock jokes, then repeatedly accepts a connection and calls handleClient to carry out the conversation with that client. The next client isn't accepted until handleClient returns.
ClientHandler — an empty class to fill in (part 1)
Riddle and KnockKnock — a riddle and a knock knock joke. (Knock knock jokes only come up in an optional extension.)
ServerStats — the server's statistics (used in part 2)
the clients:
writeup.txt — a template for your answers to the questions in the lab.
Create an Eclipse project and share it to a local Git repository:
Create a new Eclipse project named lab10. Remember that the project directory should end up under ~/cs329/dev, not in Eclipse's workspace.
Import all of the files from /classes/cs329/lab10 into your project, preserving the directory structure — writeup.txt should end up in the top level of your project and the Java files should end up under src.
Confirm that the project builds with no errors.
Create a new (local) Git repository, create/set up .gitignore, and do the initial commit. Include a meaningful commit message (e.g. "Added lab starter files").
You'll often have a server and two or three clients running at the same time, and it is useful to see all of their output at once. This is easier if you instead run the clients in terminal windows:
Eclipse compiles your code into the project's bin directory every time you save, so make sure everything is saved (and there are no compile errors) before running.
Open one terminal window for each client, and in each one, cd to the project's bin directory:
cd ~/cs329/dev/lab10/bin
Start the server (JokeServer) first, in Eclipse. Be sure to stop and restart the server each time you change its code so that it is always running the current version. (If the server reports "Address already in use", another copy of the server is still running — find the Eclipse console with the running server and stop it. To make it easier to find the right console, clear out leftover consoles from terminated programs by clicking the gray double X icon on the Console tab's toolbar.)
The clients all take the server's host and port as their first two arguments: localhost refers to the current computer and the server listens on port 9050. For example:
java RiddleClient localhost 9050
RiddleClient also takes a parameter for the number of riddles, e.g. to get three riddles:
java RiddleClient localhost 9050 3
To stop a program which is running in a terminal, press ctrl-C in its window.
Tip: you can use the up arrow key in the terminal to step back through previous commands — press Enter to run the command when you get to the one you want. This saves a lot of typing!
Run and observe, watching the server's output as you do each step:
Work through the parts below in order. Places where you need to add code are marked with TODO comments in the provided code, labeled with the specific steps (e.g. 1a, 1b) — except for part 3, where finding the places is part of the task. Answer the questions in writeup.txt as you go.
Run and observe: start the server. Start one RiddleClient but don't answer the riddle yet, instead start another RiddleClient. What happens with the second client? Now answer the first client's riddle. What happens?
Answer the part 0 questions in writeup.txt.
Commit your work.
The fix is a thread for each client: like the threaded echo server from class, the main thread only accepts connections and each client's conversation is carried out by a task run on its own thread. ClientHandler will be the task — its run method will do what JokeServer's handleClient does now.
Write code (1a): move handleClient, tellRiddle, and receiveRiddle from JokeServer to ClientHandler (cut and paste):
The body of handleClient becomes the body of run.
tellRiddle and receiveRiddle become private methods of ClientHandler. They should no longer be static — each client will have its own ClientHandler object now.
run can't have parameters (why not?), so the information that was passed to handleClient as parameters needs to get to the ClientHandler another way. How? Add what's needed to ClientHandler.
Once the information is available in ClientHandler, tellRiddle and receiveRiddle don't need it passed in as parameters either — remove those parameters (Refactor → Change Method Signature is handy for this).
Let the compiler help you: the red error markers show what still needs to be fixed. Don't forget to update the Javadoc comments.
Write code (1b): in JokeServer's main, replace the call to handleClient: create a ClientHandler for the connection, then create and start a thread to run it. Give the thread a name, e.g. "client-" + count — thread names show up in error messages and the debugger.
Run and check: repeat the experiment from part 0. What happens now?
Answer the part 1 questions in writeup.txt.
Commit your work.
The server's ServerStats object is passed to each client handler but nothing updates it so the stats reported when a client leaves (or sends the stats command) are incorrect.
Write code (2a): look at the public method headers in ServerStats to see what it provides, then add calls in ClientHandler so that each riddle told and each client served is counted. (Note that a client is counted as served (only) when its connection ends, however it ends.)
Run and check: run RiddleClient a few times, asking for several riddles each time, and check that the counts in the server's "disconnected" messages go up as you expect.
Commit your work.
The server now has several threads running at once: the main thread plus one thread for each connected client. Each thread has its own local variables and each ClientHandler has its own instance variables — but several of those variables may refer to the same object. Whenever variables or objects are shared and something changes them, thread safety is a concern.
Before changing any code, answer parts (a) and (b) of question 1 (the table) in part 3 in writeup.txt. For each item, follow the references to figure out which threads use it (where is the object created and where is it passed somewhere else?), whether it is read or changed, and whether that's a problem. Don't fill in part (c) (the "fix" parts) yet.
Recall from class the two kinds of problems to look for: a shared variable written by one thread and read by another, where the reader may not see the change (visibility), and a multi-step operation or a condition assumed to still hold across several steps, where another thread's access in between could give a wrong result (a race condition). Remember too that a multi-step operation can be a single line, such as x++ or a method call.
For library classes, check the Java API documentation: the class comment usually says whether the class is thread safe. ArrayList and Random are both worth looking up. To find the API documentation, google java 21 api and use the search box at the top of the page to locate the specific class.
Next, test. StressClient starts 20 simulated clients, each with its own thread and its own connection to the server, all running at once. It has two modes: in the default riddles mode, each simulated client asks for 25 riddles, and in submit mode, each simulated client submits 25 riddles as fast as it can. It asks the server for its stats before and after and reports how much each count (clients served, riddles told, and riddles on file) went up compared to how much it should have gone up. Lines marked with * in the output indicate a mismatch.
Run and observe: start the server, then run the stress client in riddles mode a few times:
java StressClient localhost 9050
The result will look something like this (the "should be 21" is the 20 simulated clients plus the stress client's own first stats check):
20 clients finished. How much the server's counts went up: clients served: 15 (should be 21) * riddles told: 44 (should be 500) * riddles on file: 0 (should be 0)
Run and observe: run it several more times in submit mode:
java StressClient localhost 9050 submit
These results are likely to be closer to correct than for riddles mode — one of the problems here may only show up in some runs. That's what makes race conditions so hard to find by testing: the code is wrong every time but the timing that exposes the bug only happens some of the time. Reasoning about the code is the only reliable way to find them.
Answer question 2 in the part 3 section of writeup.txt: copy one riddles result and one submit result, and connect them to the problems in your table.
Now fix the problems you found. A few things to keep in mind:
To protect shared data, every access to it — reads included — must use the same lock. Locking in some places but not others is a "sign on the door" that only works for the threads that read it.
The safest approach is for a class to "lock the door" itself: its data is private and every method which uses that data is synchronized.
When you can't change the class to lock the door, such as with a library class like ArrayList, the code which uses the object needs to lock around each use with a synchronized block. Make sure you find every place the object is used. (Search for the variable's name.) Which object should all of those blocks lock on?
Don't hold a lock any longer than necessary — in particular, not while waiting for a client to answer. (What would that do to the other clients?)
Write code: fix the problems you found — all of them, including any that didn't show up in the stress test. (There aren't any TODO markers for this part!)
Fill in part (c) of the question 1 table (in part 3 of writeup.txt) for each item with a problem: why does your change fix the kind of problem you identified in part (b)?
Run and check: restart the server and rerun the stress client several times in each mode. Every count should now come out right, every time.
Commit your work.
What happens to a client in the middle of a riddle if you stop the server? Try it.
It would be nicer to be able to shut down the server with a command and to give clients a chance to finish what they are doing first. Shutdown should be polite but firm:
ShutdownClient sends the shutdown command. Right now, the server just prints that shutdown isn't implemented yet.
The loop which accepts connections is in JokeServer's main and is run by the main thread, but the shutdown command is read by a client handler thread. So the client handler needs a way to ask the main thread to stop.
Write code (4a): add the polite shutdown pattern to JokeServer: a shutdown flag shutdown_, a shutdown() method which sets it, and a listen-for-connections loop which checks the flag and ends when it is set. Some notes:
shutdown() should close the server socket after setting the flag. (How can shutdown() get access to the server socket?)
Close the server socket makes accept() throw an IOException. Make sure that the try/catch for that IOException is in the right place — the main loop should end when the exception is thrown. Don't print an error message in that case — the exception is expected when there's been a shutdown request.
Write code (4b): in ClientHandler, call JokeServer's shutdown method when the shutdown command is read.
Run and observe: start the server, start a riddle client (but don't answer the riddle yet), and run the shutdown client. What happens? What happens when you answer the riddle? What if you start another riddle client after the shutdown request? What's going on?
Right now the main thread's loop ends, but the server keeps running until every client leaves — and a client which never answers keeps it running forever.
Write code (4c): in JokeServer's main, after the loop ends, give each client thread up to 10 seconds to finish, then exit with System.exit(0) whether or not any client threads are still running. Some notes:
To wait for the client threads, the server needs to keep track of the Thread objects it creates. How will you do that?
In class we waited with join(), which waits as long as it takes. join(ms) waits at most ms milliseconds: it returns when the thread has finished or the time is up, whichever comes first. Unlike the waiting pattern from class, call it just once for each thread, not in a loop. It can still throw InterruptedException, but nothing needs to be done in the catch.
Print messages to System.out so you can trace what the server is doing (e.g. "shutting down: waiting for clients to finish" and "server exiting").
Run and check: have two riddle clients connected, each in the middle of a riddle, when you run the shutdown client. Then answer one riddle, and leave the other alone. What happens, and when?
Answer the part 4 questions in writeup.txt, including whether shutdown has introduced any new shared-state issues (and fix them, if so).
Commit your work.
This part is optional. If you have time, add one or both of the following. Answer the corresponding questions in writeup.txt.
A status reporter: doing something periodically is a common job for a thread. Create a class StatusReporter which implements Runnable, whose run method loops forever: sleep for 5 seconds, then print the server's statistics. (Where does it get them from?) In JokeServer's main, start a thread to run a StatusReporter before the listen-for-connections loop, and make it a daemon thread. Try it with a few clients, including a stress client run, and a shutdown.
Knock knock jokes: a client asks for one by sending the command knock knock, just as it sends riddle for a riddle. Telling the joke takes some back and forth, though:
Knock knock. Who's there? Orange. Orange who? Orange you glad you asked?
so the client and server need to agree on who sends what, and in what order — a protocol. KnockKnock represents a joke, and the server already sets up a list of them.
Before writing any code, work out the protocol for telling a knock knock joke, after the client's knock knock command: each message, who sends it, and what it contains. Write it in writeup.txt. Also decide: should the server check that the client sent what it was supposed to? What should it do if the client sends something else?
Add the knock knock command to ClientHandler, following your protocol.
Write a KnockKnockClient which asks for a knock knock joke and plays the client's part, printing both sides of the conversation. (Use RiddleClient as a model.)