Network ProgrammingUnit 313 min read
Multithreading in Network Programming: Threads, Synchronization & Concurrency
Unit 3 of Network Programming explores how multithreading improves scalability and efficiency in client-server applications by handling multiple client requests concurrently, covering thread creation, synchronization, and real-world Java implementations for high-performance networking.
TAKEAWAYS:
- Multithreading allows a server to handle multiple clients simultaneously, improving throughput and responsiveness.
- Java’s
ThreadandRunnableinterfaces enable lightweight concurrency for network operations. - Synchronization mechanisms (
synchronized,ReentrantLock) prevent race conditions in shared resources. - Thread pools (
ExecutorService) optimize resource usage by reusing threads instead of creating new ones. - Network applications like eSewa and Daraz use multithreading to manage thousands of concurrent transactions.
- Deadlocks and livelocks are critical issues in multithreaded networking; proper design avoids them.
1. Introduction to Multithreading in Network Programming
Network applications often face challenges like:
- Single-threaded servers block on I/O (e.g., waiting for client requests), wasting CPU cycles.
- Scalability limits: A single thread can only handle one client at a time, leading to slow responses under load.
Multithreading solves this by:
- Running multiple threads concurrently to handle multiple clients.
- Offloading blocking operations (e.g., reading/writing sockets) to threads while the main thread processes other tasks.
1.1 Why Multithreading for Networking?
Consider a Daraz server during a sale:
- Thousands of users place orders simultaneously.
- A single-threaded server would process orders one by one, causing delays.
- A multithreaded server assigns a thread per client, processing orders in parallel.
Visualization:
2. Thread Basics in Java
Java provides two ways to create threads:
- Extending
Threadclass (not recommended for networking due to inflexibility). - Implementing
Runnableinterface (preferred for better design).
2.1 Thread Lifecycle
A thread transitions through states:
- New: Created but not started.
- Runnable: Ready to run (may be waiting for CPU).
- Running: Executing.
- Blocked/Waiting: Waiting for I/O or synchronization.
- Terminated: Execution completed.
stateDiagram-v2
[*] --> New
New --> Runnable: start()
Runnable --> Running: CPU scheduled
Running --> Blocked: I/O wait
Blocked --> Runnable: I/O completes
Running --> Terminated: finish()
[*] --> Terminated2.2 Creating a Thread for Networking
Example: A simple TCP server handling multiple clients using Runnable.
// ServerThread.java
public class ServerThread implements Runnable {
private Socket clientSocket;
public ServerThread(Socket socket) {
this.clientSocket = socket;
}
public void run() {
try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println("Echo: " + inputLine);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Trace Table:
| Step | Action | Thread State | Socket State |
|---|---|---|---|
| 1 | Thread created | New | Closed |
| 2 | start() called |
Runnable | Closed |
| 3 | accept() returns client socket |
Running | Connected (client) |
| 4 | readLine() |
Blocked (I/O) | Connected (client) |
| 5 | Data received | Running | Connected (client) |
| 6 | writeLine() |
Blocked (I/O) | Connected (client) |
| 7 | Client disconnects | Terminated | Closed |
3. Thread Synchronization
When multiple threads access shared resources (e.g., a shared ArrayList of client connections), race conditions occur:
- Thread A reads a value, Thread B modifies it, Thread A writes the old value → data corruption.
3.1 Synchronization Mechanisms
| Mechanism | Description | Example Use Case |
|---|---|---|
synchronized |
Locks a method/block to allow one thread at a time. | Shared counter for client connections. |
ReentrantLock |
More flexible than synchronized (supports fairness, tryLock). |
High-contention scenarios (e.g., NEPSE). |
volatile |
Ensures visibility of changes across threads (no atomicity). | Flags for server shutdown. |
Example: Synchronized Counter
public class ClientCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Race Condition Without Synchronization:
sequenceDiagram
participant Thread1
participant Thread2
participant SharedVar
Thread1->>SharedVar: read count (5)
Thread2->>SharedVar: read count (5)
Thread1->>SharedVar: write count (6)
Thread2->>SharedVar: write count (6)
Note over Thread1,Thread2: Lost update! Final count is 6, not 7With Synchronization:
sequenceDiagram
participant Thread1
participant Thread2
participant SharedVar
Thread1->>SharedVar: synchronized block (lock acquired)
Thread2->>SharedVar: waits for lock
Thread1->>SharedVar: read count (5)
Thread1->>SharedVar: write count (6)
Thread1->>SharedVar: unlock
Thread2->>SharedVar: synchronized block (lock acquired)
Thread2->>SharedVar: read count (6)
Thread2->>SharedVar: write count (7)4. Thread Pools (ExecutorService)
Creating threads dynamically is inefficient. Instead, use a thread pool:
- Reuses threads for multiple tasks.
- Limits resource usage (e.g., 100 threads max).
Example: Fixed Thread Pool for a Server
ExecutorService executor = Executors.newFixedThreadPool(10);
while (true) {
Socket clientSocket = serverSocket.accept();
executor.submit(new ServerThread(clientSocket));
}
Advantages:
- Avoids thread creation overhead.
- Prevents resource exhaustion (e.g., NTC’s call center handling 10,000 concurrent calls).
Disadvantages:
- Threads may starve if tasks are not distributed fairly.
- Requires tuning (e.g., pool size = CPU cores × 2).
5. Real-World Applications
5.1 eSewa: Handling Concurrent Transactions
- Idea: Uses multithreading to process thousands of payment requests simultaneously.
- How:
- Each transaction is assigned a thread from a pool.
- Synchronization ensures no double-spending (race condition prevention).
- Worked Example: Suppose 500 users initiate payments in 1 second. A single-threaded server would take 500 seconds. A multithreaded server with 100 threads processes all in ~5 seconds.
5.2 Pathao: Ride Dispatching
- Idea: Multithreading manages driver availability and ride assignments.
- How:
- One thread handles driver sign-ins.
- Another thread assigns rides to available drivers.
- Synchronization ensures no double-booking.
6. Common Pitfalls and Solutions
| Pitfall | Cause | Solution |
|---|---|---|
| Deadlock | Threads wait for each other’s locks. | Use tryLock() with timeouts. |
| Livelock | Threads repeatedly yield without progress. | Add random delays in retry logic. |
| Starvation | Some threads never get CPU time. | Use fair locks (ReentrantLock(true)). |
| Memory Leaks | Threads hold references to objects. | Use weak references or WeakHashMap. |
Example Deadlock:
// Thread 1
lock1.lock();
lock2.lock(); // Deadlock if Thread 2 holds lock2
// Thread 2
lock2.lock();
lock1.lock(); // Deadlock if Thread 1 holds lock1
Solution: Acquire locks in a fixed order.
lock1.lock();
lock2.lock(); // No deadlock if order is consistent
7. Comparison: Single-Threaded vs. Multithreaded Servers
| Feature | Single-Threaded Server | Multithreaded Server |
|---|---|---|
| Throughput | Low (1 request at a time) | High (N requests concurrently) |
| Resource Usage | Low (1 thread) | High (N threads) |
| Scalability | Poor (bottleneck at CPU) | Good (add more threads) |
| Complexity | Simple | High (synchronization, deadlocks) |
| Example Use Case | Small-scale apps (e.g., local chat) | Large-scale apps (e.g., Daraz, eSewa) |
8. Exam Tips
Define Key Terms:
- Thread: Lightweight process that runs concurrently.
- Race Condition: Undefined behavior due to unsynchronized access.
- Thread Pool: Reusable threads for efficient task scheduling.
Code Questions:
- Always show thread creation (
ThreadorRunnable). - Include synchronization (e.g.,
synchronizedblock). - Demonstrate thread pool usage (
ExecutorService).
- Always show thread creation (
Real-World Tie-Ins:
- Link multithreading to NEPSE stock trading (concurrent order processing) or WhatsApp (handling millions of messages).
Common Mistakes to Avoid:
- Forgetting to
close()sockets in threads (leaks resources). - Not handling
InterruptedException(threads can be interrupted). - Overusing threads (prefer thread pools).
- Forgetting to
Problem-Solving Approach:
- For a multithreaded server question:
- Identify shared resources (e.g., client list).
- Choose synchronization (e.g.,
synchronizedmethod). - Use a thread pool for scalability.
- Handle exceptions gracefully.
- For a multithreaded server question:
9. Worked Example: Prime Number Checker
Task: Write a multithreaded TCP server/client to check if a number is prime.
Server Code
public class PrimeServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(1234);
ExecutorService executor = Executors.newFixedThreadPool(5);
while (true) {
Socket clientSocket = serverSocket.accept();
executor.submit(new PrimeChecker(clientSocket));
}
}
}
class PrimeChecker implements Runnable {
private Socket clientSocket;
public PrimeChecker(Socket socket) { this.clientSocket = socket; }
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
int num = Integer.parseInt(in.readLine());
boolean isPrime = isPrime(num);
out.println(isPrime ? "Prime" : "Composite");
} catch (IOException e) { e.printStackTrace(); }
}
private boolean isPrime(int n) {
if (n <= 1) return false;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return false;
}
return true;
}
}
Client Code
public class PrimeClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 1234);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.println(17); // Test number
System.out.println(in.readLine()); // Output: "Prime"
socket.close();
}
}
Trace Table for Server Thread:
| Step | Action | Thread State | Socket State | Shared Resource (Prime Check) |
|---|---|---|---|---|
| 1 | Thread created | New | Closed | - |
| 2 | start() called |
Runnable | Closed | - |
| 3 | accept() returns client socket |
Running | Connected (client) | - |
| 4 | readLine() |
Blocked (I/O) | Connected (client) | - |
| 5 | Data received (17) | Running | Connected (client) | Input: 17 |
| 6 | isPrime(17) |
Running | Connected (client) | Check divisors 2-4 |
| 7 | writeLine("Prime") |
Blocked (I/O) | Connected (client) | Output: "Prime" |
| 8 | Client disconnects | Terminated | Closed | - |
10. Advanced: Thread-Local Storage
For network applications, thread-local storage (ThreadLocal) avoids shared state:
- Each thread has its own copy of a variable (e.g., client ID).
- Used in NTC’s call routing system to track each caller’s session.
ThreadLocal<Socket> clientSocket = new ThreadLocal<>();
// Set in ServerThread constructor:
clientSocket.set(socket);
// Use in run():
Socket sock = clientSocket.get();
11. Summary Diagram
Based on the TU BCA syllabus for Network Programming (CACS355), unit 3.
Discussion
Loading…