What Multithreading Has Taught Me
My first real introduction to multithreading was not some perfectly drawn diagram with arrows, cores, stacks, and a very confident explanation.
It was OSRM.
I was reading through parts of the Open Source Routing Machine codebase, mostly trying to understand how a serious backend system handles routing work. OSRM is written in C++, and it does the kind of work where performance matters immediately. You give it coordinates, and it has to return routes, tables, matches, and other responses quickly.
At first, I was just following the flow like any confused but determined junior engineer:
“Okay, this parses the request.”
“This calls the routing engine.”
“This returns the response.”
Then I started noticing something else.
The system was not thinking in a simple “do one thing, finish, then do the next thing” way. It had to serve more than one request. It had to use the machine properly. It had to protect some data and let other data be read safely.
That was the moment multithreading stopped feeling like a scary topic hiding in an operating systems textbook.
It became a thing real backend code uses because real users are impatient.
Fair enough, honestly.
My First Guess Was Too Simple
Before seeing it in real code, my understanding was basically:
More threads = faster program.
That was it. That was the whole mental model. Very elegant. Also very incomplete.
Now I think of it more like this:
Multithreading lets a program have multiple lines of work happening around the same time.
One request might be calculating a route. Another might be waiting for I/O. Another might already be preparing a response.
Instead of making everything queue up behind one long-running task, the program can keep moving.
But those pieces of work may share the same process, the same memory, and sometimes the same objects. That is where the “faster program” idea gets humbled very quickly.
The Small Example That Made It Click
Imagine a tiny counter in Java:
class Counter {
private int value = 0;
public void increment() {
value++;
}
public int getValue() {
return value;
}
}
Looks innocent.
Too innocent.
If one thread calls increment(), no problem. If many threads call it at the same time, things can get weird.
The line value++ looks like one action, but it is closer to:
read value
add one
write value back
So two threads can both read 0, both add 1, and both write back 1.
You expected 2.
You got 1.
The program did not crash. It did not scream. It just quietly gave you the wrong answer, which is somehow more annoying.
That is the first thing multithreading taught me: some bugs do not look like bugs when you read the code from top to bottom.
The problem is in the timing.
Locking, Without Making It Weird
One way to protect that counter is to lock the part where shared data changes.
In Java, the simplest version is synchronized:
class SafeCounter {
private int value = 0;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}
This basically says:
“Only one thread at a time can enter these methods for this object.”
That is the main idea behind locking. You are not making the whole program single-threaded. You are only protecting the small part where shared state can be damaged.
Very boring. Very useful.
Mutexes Are The Same Idea In A Different Jacket
I used to see the word “mutex” and immediately feel like the conversation had left me behind.
Then I realized it is still the same basic idea: protect the shared thing so only one thread touches it at a time.
In Java, ReentrantLock gives you a more explicit lock:
import java.util.concurrent.locks.ReentrantLock;
class CounterWithLock {
private final ReentrantLock lock = new ReentrantLock();
private int value = 0;
public void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
}
This looks a bit more serious because you manually lock and unlock.
The important part is the finally.
If something goes wrong inside the locked section, the lock still gets released. Without that, one thread can fail halfway through and leave everyone else waiting forever.
That is not the technical definition, but it is the image that stuck in my head.
Shared State Is Where The Drama Starts
Shared state is any data multiple threads can access.
Not scary by itself.
But if that data can change, you have to pay attention.
Here is a simple example:
class BookingService {
private int availableSeats = 1;
public boolean bookSeat() {
if (availableSeats > 0) {
availableSeats--;
return true;
}
return false;
}
}
This looks fine until two threads call bookSeat() at almost the same time.
Both can see availableSeats > 0.
Both can enter the if.
Congratulations, you sold one seat to two people.
Now you have a technical problem and a customer support problem. A beautiful combo.
A basic fix:
class BookingService {
private int availableSeats = 1;
public synchronized boolean bookSeat() {
if (availableSeats > 0) {
availableSeats--;
return true;
}
return false;
}
}
Again, the point is not that synchronized is always the best solution.
The point is that the check and the update need to be protected together.
That was a useful lesson for me. Sometimes the dangerous part is not one line. It is the small group of lines that must stay together.
Debugging Multithreaded Code Feels Different
A normal bug can be annoying, but at least it usually has manners.
You give the same input.
It fails the same way.
You stare at it long enough.
Eventually, something confesses.
Multithreaded bugs can be less cooperative.
They might happen only under load. They might disappear when you add logs because logging changes the timing. They might refuse to show up locally and then appear in production like they pay rent there.
That part was humbling.
It taught me that with multithreading, I should not only ask:
“What does this code do?”
I should also ask:
“What else could be touching this at the same time?”
That one question changes how you read code.
Final Thoughts
Multithreading is harder than it looks because the code you read is only part of the story.
The other part is timing.
Two pieces of code can both be correct alone and still be unsafe together.
That is the part I did not understand at first.
Reading OSRM helped because it showed me multithreading in context. Not as a classroom exercise. Not as “Thread A prints hello and Thread B prints world.” But as something a backend system needs when it has real work to do.
I still move slowly when I read concurrent code.
I still double-check my assumptions.
I still get suspicious when shared data is being changed from different places.
But that feels like progress.
For me, multithreading went from “advanced magic” to “a powerful tool that needs adult supervision.”
And for a junior engineer learning from production code, that is a pretty good place to start.