09/06/2026
I still remember the first time I chased a bug that only appeared under production load.
Everything looked correct. The code was simple:
counter = counter + 1
Yet somehow the final count was lower than expected.
The reason? Two threads were updating the same value at the same time.
Imagine counter = 5.
* Thread A reads 5
* Thread B reads 5
* Thread A writes 6
* Thread B writes 6
Final result: 6
Expected result: 7
One update simply disappeared.
This is one of the most common race conditions in concurrent programming. The problem isn’t the increment itself—it’s that the read, modify, and write operations happen separately.
That’s where synchronization comes in:
private val lock = Any()
synchronized(lock) {
counter = counter + 1
}
Now only one thread can execute that block at a time. While Thread A is inside, Thread B has to wait. Both increments are applied correctly.
A few things I’ve learned about synchronized over the years:
✅ The lock object is simply a shared key. What matters is that all related code uses the same lock instance.
✅ Using different lock objects means there is no protection at all.
✅ It’s reentrant, so the same thread can acquire the same lock multiple times without deadlocking itself.
✅ Synchronizing only some accesses is almost as bad as synchronizing none. Every read and write that matters must follow the same rule.
✅ Locks aren’t free. Under heavy contention, alternatives like AtomicInteger, ConcurrentHashMap, or coroutine Mutex can perform much better.
✅ Avoid holding locks during network calls, database operations, or other slow I/O. Synchronize only the critical section that actually needs protection.
Most concurrency bugs don’t show up during development.
They show up at 2 AM, under real traffic, when everything “should” be working.
What’s the hardest race condition or threading issue you’ve had to debug?
ThreadSafety BackendDevelopment Programming