Concurrency and Multithreading in Kotlin: Advanced Level
\nIn modern Kotlin development, especially for server-side and Android applications, proper concurrency management is key to performance and responsiveness. At an advanced level, it's not enough to simply launch a coroutine. You need to understand how to efficiently synchronize data access, manage threads, and avoid "deadly" locks. In this article, we will cover advanced techniques: from the intricacies of kotlinx.coroutines to thread-safe collections and channels.
1. Deep Dive into Coroutines: Structured Concurrency and Scopes
\nThe foundation of advanced work is Structured Concurrency. This is a principle where the lifecycle of a coroutine is tied to a scope. If a parent coroutine is canceled, all its child tasks are automatically terminated. This prevents memory leaks and uncontrolled processes.
\nAt an advanced level, you should create your own scopes rather than relying on the global GlobalScope.
import kotlinx.coroutines.*\nimport kotlin.coroutines.CoroutineContext\n\nclass DataRepository {\n // Custom scope with dispatcher and exception handler\n private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, throwable ->\n println("Unhandled error: ${throwable.message}")\n })\n\n fun loadData() {\n scope.launch {\n // child coroutines\n val data = async { fetchFromNetwork() }\n val cache = async { loadFromCache() }\n process(data.await(), cache.await())\n }\n }\n\n fun cleanup() {\n scope.cancel() // cancel all coroutines in the scope\n }\n}\n\nUse SupervisorJob when a failure in one child coroutine should not cancel the others. This is critical for UI layers or parallel independent requests.
2. Thread-Safe Collections and Atomic Operations
\nEven with coroutines, if you work with shared mutable data, synchronization is needed. Kotlin offers several approaches:
\n- \n
- Atomic types (
AtomicInteger,AtomicReference) — for simple counters and flags. \n - Thread-Safe collections (
ConcurrentHashMap,CopyOnWriteArrayList) — for complex structures. \n - Mutex from
kotlinx.coroutines— for critical sections inside coroutines. \n
import kotlinx.coroutines.*\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass SafeCounter {\n private val mutex = Mutex()\n private var counter = 0\n\n suspend fun increment() {\n mutex.withLock {\n counter++ // critical section\n }\n }\n\n fun getCounter(): Int = counter\n}\n\nclass CacheManager {\n private val cache = ConcurrentHashMap<String, String>()\n private val hitCount = AtomicInteger(0)\n\n fun put(key: String, value: String) {\n cache[key] = value\n }\n\n fun get(key: String): String? {\n val result = cache[key]\n if (result != null) hitCount.incrementAndGet()\n return result\n }\n}\n\nImportant: Mutex is a suspend function, so it does not block a thread, but suspends the coroutine. This is more efficient than synchronized.
3. Channels and Flow: Reactive Data Streams
\nFor transferring data between coroutines, use Channels (an analog of BlockingQueue for coroutines) and Flow (a cold asynchronous stream).
Channels: Producer-Consumer
\nimport kotlinx.coroutines.channels.*\nimport kotlinx.coroutines.*\n\nfun Corout