Why network code is concurrent, and how to start a thread
◈ 4 cardsNetwork programs are I/O-bound, not CPU-bound; Thread subclass vs Runnable; why start() creates concurrency and run() silently does not.
Your program is not slow — it is waiting
Put the numbers side by side. A modern CPU retires billions of instructions per second. A DNS lookup for a name it has never seen costs tens of milliseconds. Opening a TCP connection across a continent costs a hundred or more. A busy origin server can take seconds to answer. Through every one of those pauses your thread is parked inside a system call, holding no CPU and moving no bytes.
That is what I/O-bound means: the bottleneck is the wire, not the processor. It is also why tuning the parsing loop in a network program is almost always wasted effort. You are not trying to compute faster — you are trying to have several waits happening at the same time. Ten hostnames resolved one after another cost ten waits; ten resolved concurrently cost roughly one. Nothing about the CPU changed.
Two ways to hand a thread some work
A thread with a small t is an independent path of execution inside the virtual machine. A Thread with a capital T is the object that represents one. The relationship is one to one, and every thread runs exactly one method — run() — then dies when it returns. A single-threaded program exits when main() returns; a multithreaded one exits when main() and every non-daemon run() have returned.
There are two ways to supply that method:
- Subclass
Threadand overriderun(). Compact, but it welds your task to the machinery that runs it. - Implement
Runnable— an interface whose whole content isvoid run()— and pass the instance to aThreadconstructor.
Prefer Runnable. It keeps the task separate from the thing that executes the task, which is exactly what lets you hand the same object to a thread pool in L3.5 without editing a line of it. Since Java 8 a stateless Runnable can be a lambda.
start(), never run()
start() asks the JVM for a new path of execution and returns immediately; the new thread then calls run(). Calling run() yourself compiles, runs, produces the right answer, and creates no concurrency at all — the body executes on the calling thread, in order, exactly as if you had written a plain method call. Nothing throws. Nothing warns you. The only symptom is that the program is as slow as it was before.
A thread is also not free: each one carries a stack (commonly half a megabyte or more) plus scheduler bookkeeping, so a JVM exhausts memory somewhere in the low thousands of live threads. One thread per unit of work is fine for ten hostnames and fatal for two hundred thousand log lines — which is the problem L3.5 solves.
Worked example — three lookups, one wait
You have three hostnames and want each printed with its address. Serially that is three DNS round trips end to end. Instead, wrap the lookup in a Runnable that takes one host, start three of them, and all three waits overlap: the main thread issues start() three times and is then idle while the resolver library blocks on three sockets at once. Wall-clock cost drops from the sum of the three waits to roughly the longest one.
Two details matter even in this toy. InetAddress.getByName throws the checked UnknownHostException, and run() is declared to throw nothing — so the task must catch it internally; there is no caller to propagate it to. And the print order is now unpredictable: the fastest lookup prints first. If order matters, you need the machinery of L3.2 and L3.6, not another thread.