Introduction to Concurrency and Multithreading in Kotlin
Modern applications increasingly require executing multiple tasks simultaneously — from processing network requests to working with databases. In the Kotlin language, which is actively used for Android development, server applications (Ktor, Spring) and even desktop solutions, powerful tools for working with concurrency and multithreading are implemented.
In this article, we will break down key concepts: traditional threads (Threads), coroutines (Coroutines) — a modern approach to asynchrony in Kotlin, as well as synchronization mechanisms. You will learn when to use threads and when to use coroutines, and get ready-made code examples.
1. Traditional Threads in Kotlin
Kotlin is fully compatible with Java, so you can use the standard Thread and Runnable classes. However, Kotlin adds syntactic sugar, making the code more concise.
Creating and starting a thread
The basic way to create a thread is to pass a lambda to the Thread constructor:
fun main() { val thread = Thread { println("Thread started: ${Thread.currentThread().name}") for (i in 1..5) { println("Count: $i") Thread.sleep(500) } } thread.start() println("Main thread continues working") thread.join() // wait for the thread to finish println("Main thread finished")}In this example, we created a thread that prints numbers from 1 to 5 with a 500 ms delay. The join() method forces the main thread to wait for the child thread to complete.
Problems with threads
- High resource consumption — each thread requires a separate memory stack (usually 1 MB).
- Complex synchronization — when working with shared data, it is easy to cause a race condition.
- Limited quantity — you cannot run tens of thousands of threads on one device without losing performance.
This is why coroutines appeared in Kotlin — a lighter alternative.
2. Coroutines — Modern Concurrency in Kotlin
Coroutines are lightweight threads that can suspend their execution without blocking the system thread. They are the main tool for asynchronous programming in Kotlin.
Adding coroutines dependency
To work with coroutines, you need to add a dependency in build.gradle.kts:
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")}Basic coroutine example
import kotlinx.coroutines.*
fun main() = runBlocking { println("Start working in thread: ${Thread.currentThread().name}")
launch { // launch a new coroutine delay(1000) // non-blocking delay println("Coroutine: ${Thread.currentThread().name}") }
println("Main coroutine continues working") delay(2000) // wait for the child coroutine to finish println("End")}Key points:
runBlocking— a bridge between regular code and coroutines (blocks the current thread).launch— launches a coroutine that does not return a result.delay— non-blocking delay, unlikeThread.sleep().
Async and await for getting results
If a coroutine should return a value, use async:
fun main() = runBlocking { val deferred1 = async { delay(1000) "Result 1" } val deferred2 = async { delay(500) "Result 2" }
println("Waiting for results...") val result1 = deferred1.await() val result2