-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
202: Common Kotlin Interview Questions on Coroutines vs Threads
I've sat through dozens of technical interviews, and there is one answer that makes me immediately lean in—not because it's right, but because it's so common. When I ask, "What is the difference between a thread and a coroutine?" the candidate almost always says: "Coroutines are just lightweight threads."
On the surface, that sounds correct. But it's a dangerous simplification. If you tell an interviewer that, you're essentially saying "a car is just a fast bicycle." It misses the fundamental architectural difference: how they handle waiting.
The "Lightweight Thread" Myth vs. The Reality of Memory
If coroutines were just "small threads," we'd still be limited by the OS. Let's look at why that's wrong. Every JVM thread carries a hefty memory overhead—usually around 1MB for the stack. If you try to spawn 100,000 threads to handle 100,000 concurrent API requests, your application will crash with an OutOfMemoryError long before you hit that number.
// This will likely crash your JVM
repeat(100_000) {
Thread {
Thread.sleep(1000)
println("Done")
}.start()
}
Now, try that with coroutines using Dispatchers.Default. It won't crash. Why? Because you aren't actually creating 100,000 threads. You're creating 100,000 objects (continuations) that represent a piece of work. These objects are stored on the heap, which is vastly cheaper than allocating a native OS stack for every single task. I usually tell my juniors to think of it this way: a thread is like a dedicated physical lane on a highway, while a coroutine is more like a car that can pull over into a parking spot when it needs to wait for a green light, letting other cars pass by.
Blocking the Thread vs. Suspending the Coroutine
This is the "million-dollar" interview point. When a thread calls Thread.sleep() or waits for a database response, that thread is blocked. It sits there, doing absolutely nothing, but it still consumes that 1MB of memory and prevents the OS from using it for anything else.
Coroutines use suspension. When a coroutine hits a suspend function (like delay() or a Ktor network call), it doesn't block the thread it's running on. Instead, it "unhooks" itself from the thread, saves its local state, and lets the thread go back to the pool to do other work. When the timer expires or the data returns, the coroutine is "resumed" on any available thread from the dispatcher.
// The "Blocking" Way (Thread-per-request)
fun fetchUserBlocking() {
val user = api.getUser() // Thread stops here and waits. Wasteful.
println(user)
}
// The "Suspending" Way (Coroutine)
suspend fun fetchUserSuspending() {
val user = api.getUser() // Coroutine pauses; thread is freed for others.
println(user) // Resumes here once data is ready.
}
If you're asked about "Context Switching" in an interview, mention this. Switching between OS threads requires a transition into the kernel, which is expensive. Switching between coroutines happens in the Kotlin runtime (user-space), which is orders of magnitude faster.
The "One-to-Many" Mapping
To wrap this up, you need to be clear on the mapping. Threads are managed by the Operating System. Coroutines are managed by the Kotlin library. A single thread can execute thousands of coroutines over its lifetime by jumping between them as they suspend and resume.
Don't get confused: coroutines don't magically make your code run in parallel on their own. If you launch 1,000 coroutines on Dispatchers.Main, they are all running on one single thread. They just aren't blocking it. Parallelism requires a multi-threaded dispatcher like Dispatchers.Default or Dispatchers.IO, but the efficiency comes from the fact that those threads are never sitting idle while waiting for I/O.
📋 Practical Task
Exercise: Converting a Blocking Thread-Pool to a Suspending Coroutine System
You have been handed a legacy "User Notification System" that uses a FixedThreadPool. The current implementation is crashing under high load because it creates too many threads to handle the simulated network latency of sending emails.
Your Task:
- Remove the
ExecutorServiceandThread.sleep(). - Implement the
sendNotificationlogic usingsuspendfunctions anddelay(). - Use
coroutineScopeandlaunchto trigger 1,000 notifications concurrently without crashing the JVM or blocking the main thread.
// LEGACY CODE TO FIX
class NotificationService {
private val executor = Executors.newFixedThreadPool(10)
fun sendNotification(userId: String) {
executor.execute {
println("Sending to $userId on ${Thread.currentThread().name}")
Thread.sleep(500) // Simulating network delay
println("Sent to $userId")
}
}
}
fun main() {
val service = NotificationService()
repeat(1000) { i ->
service.sendNotification("User-$i")
}
}
Success Criteria: Your final code should be able to handle 1,000 (or even 10,000) calls to sendNotification almost instantaneously without creating 1,000 OS threads, and it should utilize Dispatchers.IO for the simulated network work.
There are no comments for now.