Skip to Content
Course content

221: Java Concurrency Utilities from Kotlin

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern with developers moving from Java to Kotlin: they discover Coroutines and suddenly decide that the entire java.util.concurrent package is a relic of the past. There's this lingering feeling that because Coroutines handle scheduling so elegantly, we no longer need the "clunky" Java utilities for synchronization.

The Myth: Coroutines Handle Shared State Automatically

The misconception is that if you're using launch or async, you're safe from race conditions because you're "using the Kotlin way." That is dangerously wrong. Coroutines are not a magic shield for your data; they are a way to manage execution. If two coroutines running on Dispatchers.Default try to mutate the same HashMap, you're going to get a ConcurrentModificationException or, even worse, silent data corruption.

// DO NOT DO THIS
val sharedMap = mutableMapOf<String, Int>()
val scope = CoroutineScope(Dispatchers.Default)

repeat(1000) { i ->
    scope.launch {
        // This is a race condition. HashMap is not thread-safe.
        sharedMap["key"] = (sharedMap["key"] ?: 0) + 1
    }
}

In the snippet above, the mutableMapOf (which is a java.util.HashMap under the hood) is being hit by multiple threads simultaneously. The fact that we're using coroutines doesn't change the fact that the underlying memory is being contested.

The Reality: JUC Primitives for Atomic State

When you need to manage shared state across multiple coroutines, you still lean on the Java Concurrency Utilities (JUC). Specifically, Atomic variables and Concurrent collections are your best friends here. They are lightweight, highly optimized, and work perfectly within Kotlin.

If you're tracking a simple counter or a flag, don't reach for a Mutex or a lock immediately. Reach for an AtomicInteger or AtomicBoolean. They use CPU-level "Compare-and-Swap" (CAS) operations, which are significantly faster than blocking a thread.

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

class MetricsCollector {
    // Thread-safe map for counting occurrences of events
    private val eventCounts = ConcurrentHashMap<String, AtomicInteger>()

    fun recordEvent(name: String) {
        // computeIfAbsent ensures we atomically create the AtomicInteger if it doesn't exist
        eventCounts.computeIfAbsent(name) { AtomicInteger(0) }
            .incrementAndGet()
    }

    fun getCount(name: String) = eventCounts[name]?.get() ?: 0
}

I prefer ConcurrentHashMap over wrapping a standard map in a synchronized block because it allows concurrent reads and striped writes. It's just more efficient.

Avoid Blocking Locks in a Suspending World

Here is where you need to be careful. While AtomicInteger and ConcurrentHashMap are great, you should almost never use ReentrantLock or synchronized blocks inside a suspending function.

Why? Because these are blocking primitives. If a coroutine hits a synchronized block and has to wait, it blocks the underlying thread. If you're using a limited thread pool (like Dispatchers.Main or a fixed-size pool), you can accidentally starve your entire application, leading to a deadlock or a complete freeze. If you truly need a lock that "waits" without blocking the thread, use Kotlin's kotlinx.coroutines.sync.Mutex. But for state updates, the JUC atomic classes are usually the better tool for the job.




📋 Practical Task

Building a Thread-Safe Request Rate Limiter

You are building a system that tracks how many API requests are sent per endpoint to prevent overloading your backend. Because requests arrive concurrently across many coroutines, you must ensure the tracking is thread-safe and high-performance.

Your Goal: Implement a class RequestTracker that uses java.util.concurrent utilities to track request counts.

  • Use a ConcurrentHashMap to map endpoint URLs (Strings) to their respective request counts.
  • Use AtomicLong to store the count for each endpoint to ensure increments are atomic.
  • Implement a function recordRequest(url: String) that increments the count for the given URL.
  • Implement a function getRequestCount(url: String): Long that returns the current count.

Requirements:
1. Ensure that recordRequest does not lose increments when called from 100+ concurrent coroutines.
2. Avoid using synchronized blocks or ReentrantLock.
3. Use computeIfAbsent to handle the initialization of the AtomicLong for new URLs.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.