Concurrency and Multithreading in Kotlin: From A to Z
Modern applications increasingly require executing multiple tasks simultaneously. In Kotlin, as in other JVM languages, multithreading is traditionally implemented through the Thread class. However, the language offers a much more powerful and elegant tool — coroutines. In this article, we will cover all aspects of concurrency and multithreading in Kotlin: from basic concepts to advanced techniques.
1. Basics of Multithreading: Thread and Runnable
Let's start with the classic approach. In Kotlin, as in Java, you can create a thread directly:
fun main() { val thread = Thread { for (i in 1..5) { println("Thread: $i") Thread.sleep(500) } } thread.start() println("Main thread finished")}However, manual thread management has drawbacks:
- High overhead — creating and destroying threads is expensive.
- Complex synchronization — it's easy to introduce a race condition.
- Limited number of threads — the system can only create a limited number of threads.
To synchronize access to shared data, the synchronized keyword or ReentrantLock locks are used:
class Counter { private var count = 0 @Synchronized fun increment() { count++ }}2. Coroutines — A Modern Approach to Concurrency
Coroutines are lightweight threads that solve the problems of classic multithreading. They are not tied to system threads and can execute on a single thread, switching between tasks.
2.1. First Coroutine
To work with coroutines, you need to add the dependency org.jetbrains.kotlinx:kotlinx-coroutines-core. Example of a simple coroutine:
import kotlinx.coroutines.*
fun main() = runBlocking { launch { delay(1000L) println("World!") } println("Hello,")}Here, runBlocking is a bridge between regular code and coroutines. launch starts a new coroutine, and delay is an analog of Thread.sleep(), but it does not block the thread.
2.2. Suspend Functions
The suspend keyword allows pausing the execution of a function without blocking the thread:
suspend fun fetchData(): String { delay(2000L) // simulating a network request return "Data received"}
fun main() = runBlocking { val result = async { fetchData() } println("Waiting...") println(result.await())}2.3. Structured Concurrency
Kotlin supports structured concurrency — all child coroutines automatically complete before the parent coroutine completes:
fun main() = runBlocking { coroutineScope { launch { delay(1000L) println("Task 1") } launch { delay(500L) println("Task 2") } } println("All tasks completed")}3. Advanced Techniques: Channel, Flow, and Dispatchers
3.1. Channels
Channels allow data exchange between coroutines. They are an analog of a queue:
fun main() = runBlocking { val channel = Channel<Int>() launch { for (i in 1..5) { channel.send(i * i) } channel.close() } for (value in channel) { println(value) }}3.2. Flow — Cold Data Streams
Flow is a reactive stream that emits values on demand:
fun simpleFlow(): Flow<Int> = flow { for (i in 1..3) { delay(