A hands-on educational project that demonstrates three progressively advanced concurrency models for building TCP servers in Java — moving from a naive single-threaded server, to a thread-per-request model, and finally to a production-style thread-pool architecture.
- Overview
- Architecture
- Modules
- Getting Started
- How It Works
- Performance Comparison
- Contributing
- License
This repository explores the evolution of server concurrency in Java through three self-contained implementations:
| Approach | Concurrency Model | Scalability | Resource Usage |
|---|---|---|---|
| Single-Threaded | Sequential — one client at a time | ❌ Poor | ✅ Minimal |
| Multithreaded | Thread-per-request | ❌ High (unbounded threads) | |
| Thread Pool | Fixed-size ExecutorService pool |
✅ Good | ✅ Controlled |
Each module is fully independent and can be compiled and run on its own.
MultithreadedWebServer/
├── SingleThreaded/
│ ├── Server.java # Blocking, sequential server
│ └── Client.java # Single-request client
├── Multithreaded/
│ ├── Server.java # Thread-per-request server
│ └── Client.java # Multi-threaded client (100 concurrent requests)
├── ThreadPool/
│ └── Server.java # Fixed thread-pool server (ExecutorService)
└── README.md
A basic TCP server that accepts and processes one connection at a time. While one client is being served, all other clients must wait in the connection queue.
Key characteristics:
- Listens on port
8010 - 20-second socket timeout
- Processes clients sequentially in the main thread
- Simple
PrintWriter/BufferedReaderI/O
⚠️ Limitation: This model cannot handle concurrent clients. If a client takes a long time to process, it blocks all subsequent connections.
An improved server that spawns a new thread for every incoming connection, allowing multiple clients to be served simultaneously.
Key characteristics:
- Listens on port
8010 - 70-second socket timeout
- Creates a new
Threadper client via lambda +Consumer<Socket> - Ships with a load-testing client that fires 100 concurrent threads
⚠️ Limitation: Unbounded thread creation — under heavy load this can exhaust system resources (memory, file descriptors) and crash the JVM.
The most robust implementation, using Java's ExecutorService with a fixed thread pool to limit concurrent thread usage while still handling multiple clients.
Key characteristics:
- Listens on port
8010 - 70-second socket timeout
- Uses
Executors.newFixedThreadPool(10)(configurable pool size) - Graceful shutdown of the thread pool via
shutdown()in afinallyblock
✅ Production-ready pattern: Thread pools prevent resource exhaustion and provide predictable performance under load.
- Java JDK 8+ (uses lambdas,
Consumer<T>,ExecutorService) - A terminal / command prompt
Each module can be compiled and run independently. Navigate to the desired module directory and follow the steps below.
# Terminal 1 — Start the server
cd SingleThreaded
javac Server.java
java Server
# Terminal 2 — Start the client
cd SingleThreaded
javac Client.java
java Client# Terminal 1 — Start the server
cd Multithreaded
javac Server.java
java Server
# Terminal 2 — Start the client (spawns 100 concurrent connections)
cd Multithreaded
javac Client.java
java Client# Terminal 1 — Start the server
cd ThreadPool
javac Server.java
java Server
# Terminal 2 — Use the Multithreaded Client to test
cd Multithreaded
javac Client.java
java ClientTip: You can also test any server with
telnet localhost 8010orcurl telnet://localhost:8010.
┌────────┐ ┌────────────┐
│ Client │───TCP──▶│ Server │
│ │◀────────│ (port 8010)│
└────────┘ "Hello └────────────┘
from server"
SingleThreaded: Main thread handles each client sequentially
Multithreaded: New Thread() per accept() — unbounded
ThreadPool: ExecutorService.execute() — bounded pool of N threads
- The Server binds to port
8010and enters an infiniteaccept()loop. - When a Client connects, the server sends a greeting:
"Hello from server <address>". - The Client reads the response and prints it to stdout.
- The connection is closed, and the server is ready for the next client.
The key difference between modules is how step 2 is dispatched:
| Module | Dispatch Mechanism |
|---|---|
| SingleThreaded | Inline in main thread |
| Multithreaded | new Thread(() -> ...).start() |
| ThreadPool | threadPool.execute(() -> ...) |
Below is a qualitative comparison when tested with the 100-connection client:
| Metric | SingleThreaded | Multithreaded | ThreadPool (size=10) |
|---|---|---|---|
| Throughput | ~1 req/s | High (all at once) | High (batched) |
| Latency (per client) | High (queued) | Low | Low-Medium |
| Memory usage | Constant | Spikes (100 threads) | Bounded |
| Thread safety | N/A | Risk of exhaustion | Safe |
| Recommended for | Learning only | Small demos | Production use |
Contributions are welcome! Here are some ideas for enhancements:
- Add HTTP request parsing (GET, POST)
- Implement a static file server
- Add logging with
java.util.logging - Create benchmarking scripts with JMH
- Add
NIO(non-blocking I/O) implementation as a fourth module - Dockerize the servers
To contribute:
- Fork this repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m "Add my feature") - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
This project is open source and available under the MIT License.
Made with ☕ and Java | ⭐ Star this repo if you found it useful!