What I Actually Needed to Know About Java Threading to Pass My IKM Assessment

French software engineer building a new life in Japan. My journey here is a big challenge—from learning the language to navigating the tech scene. I use this blog as a space to share what I'm learning, both in tech and in life.
WHY I NEEDED THIS
I had an IKM assessment coming up — one of those technical screeners that stands between you and the next interview round. The test covered Java fundamentals, but I kept hitting threading questions that felt like trick questions. Not "write a producer-consumer" but "what happens when you call run() instead of start()?" The kind of gotchas that separate people who've actually debugged concurrency issues from people who've just read the theory.
WHAT I GOT WRONG FIRST
I thought threading was about knowing the big patterns — executors, thread pools, concurrent collections. That's what every tutorial focuses on. But the assessment questions were different. They tested edge cases and state transitions I'd never thought about.
The run() vs start() question threw me completely. I knew start() was "the right way" but couldn't articulate why calling run() directly would work but defeat the entire purpose. I thought it would error out or do nothing. Turns out it just runs synchronously in your current thread — perfectly valid Java, completely missing the point of threading.
Same with ThreadLocal. I'd seen it in Spring code but never understood what problem it actually solved. I thought it was just "a variable that threads can share" which is backwards — it's specifically for not sharing.
WHAT ACTUALLY WORKED
The breakthrough came when I stopped thinking about threading as "parallel execution" and started thinking about thread states as a state machine. There are exactly six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. Every threading concept maps to transitions between these states.
When you call start(), you transition NEW → RUNNABLE. The JVM creates a new thread and schedules it. When you call run() directly, nothing transitions — the thread object stays in NEW state forever. You're just calling a method. This explains why this code works but isn't concurrent:
Thread thread = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.run(); // Prints "main" — same thread!
System.out.println(thread.getState()); // Still NEW
ThreadLocal clicked when I understood it as per-thread storage with no sharing. Each thread gets its own copy of the variable. The use case isn't "threads communicating" but "threads needing context without passing parameters everywhere." Like storing a user ID for the duration of an HTTP request without threading it through every method call.
StampedLock was the trickiest. I knew ReadWriteLock but StampedLock's "optimistic read" seemed like overengineering until I saw the pattern: try reading without locking, validate afterward, fall back to a real lock only if validation fails. It's faster because the common case (no concurrent writes) costs almost nothing:
long stamp = lock.tryOptimisticRead(); // No lock acquired
int value = data; // Just read
if (!lock.validate(stamp)) { // Did someone write during our read?
stamp = lock.readLock(); // Fall back to real lock
try {
value = data;
} finally {
lock.unlockRead(stamp);
}
}
The assessment also tested string interning, which I'd never used. The key insight: intern() puts strings in a shared pool so identical strings point to the same object. Lets you use == instead of equals() for content comparison. Useful when you have thousands of duplicate strings (like "admin", "user" role strings repeated everywhere) and want to save memory.
THE 20% THAT COVERS 80% OF CASES
For threading assessments, know these cold:
Thread states: NEW (created), RUNNABLE (ready/running), BLOCKED (waiting for synchronized lock), WAITING/TIMED_WAITING (waiting on condition), TERMINATED (done). Know which methods cause which transitions.
start() vs run(): start() creates a new thread. run() is just a method call in the current thread. Both "work" but only one is actually concurrent.
ThreadLocal: Each thread gets its own copy. Use for per-request context like user IDs or transaction state. Never share ThreadLocal variables between threads — that defeats the purpose.
Lock semantics: synchronized blocks cause BLOCKED state. Thread.sleep() causes TIMED_WAITING. Object.wait() causes WAITING. StampedLock's optimistic read is faster because it doesn't block readers when there are no writers.
STILL FUZZY ON
I still don't have an intuition for when StampedLock's complexity is worth it versus just using ReadWriteLock. The performance gain matters for high-throughput systems, but how high is "high enough"?
Also uncertain about ThreadLocal memory leaks. I know you're supposed to call remove() in finally blocks to prevent leaks in thread pool scenarios, but I haven't debugged one myself so it's still theoretical knowledge.
The string pool internals are fuzzy too. I know intern() puts strings there and literals are there by default, but I don't know the pool's size limits or when it gets garbage collected. Probably doesn't matter for most code, but assessments love asking about edge cases.
🤖 Generated by AI from Claude's logs.






