Android Nomad #47 - Threading Models Advanced

Multithreading concepts in Kotlin

Android Nomad #47 - Threading Models Advanced

In modern Kotlin development, understanding multithreading is essential for creating high-performance applications that can handle concurrent tasks efficiently. Multithreading enables the execution of multiple tasks simultaneously, making your application more responsive and able to take full advantage of modern multicore processors. In this guide, we'll dive deep into key multithreading concepts in Kotlin and provide code examples to illustrate how each concept works.

1. Atomic Assignments

Atomic assignments ensure that a variable update is indivisible, meaning it cannot be interrupted mid-operation by another thread.

In Kotlin, atomic variables are available in the Atomic* classes from kotlinx.atomicfu or Java’s java.util.concurrent.atomic package:

import java.util.concurrent.atomic.AtomicInteger

fun main() {
    val atomicCounter = AtomicInteger(0)

    // Atomic increment
    val newValue = atomicCounter.incrementAndGet()
    println("Atomic Incremented Value: $newValue")
}

This ensures that incrementing atomicCounter is thread-safe and can be used safely in concurrent environments.

2. Thread Safety & Synchronized

When multiple threads access shared data, thread safety must be ensured to avoid unpredictable behavior. The synchronized block in Kotlin helps achieve this by allowing only one thread to access a critical section at a time.

val lock = Any()
var counter = 0

fun increment() {
    synchronized(lock) {
        counter++
    }
}

Here, only one thread can increment the counter at a time, preventing race conditions.

3. Wait & Notify

In Kotlin, wait and notify can be used with the synchronized block for inter-thread communication. wait pauses a thread until another thread calls notify, signaling it to resume.

val lock = Object()

fun waitingThread() {
    synchronized(lock) {
        println("Waiting...")
        lock.wait() // Release lock and wait
        println("Resumed after notification")
    }
}

fun notifyingThread() {
    synchronized(lock) {
        println("Notifying...")
        lock.notify() // Notify the waiting thread
    }
}

This is useful when one thread must wait for a certain condition to be met by another thread.

4. Interrupting Threads

Threads can be interrupted to stop or redirect them to other tasks. Kotlin provides an isInterrupted flag and the interrupt() method for this.

val thread = Thread {
    while (!Thread.currentThread().isInterrupted) {
        println("Working...")
        Thread.sleep(1000)
    }
}

fun main() {
    thread.start()
    Thread.sleep(3000)
    thread.interrupt() // Interrupt the thread
}

This stops the thread gracefully after 3 seconds.

5. Volatile

The volatile keyword ensures that updates to a variable are immediately visible to all threads, preventing caching issues across CPU cores.

@Volatile
var isRunning = true

val thread = Thread {
    while (isRunning) {
        println("Running...")
    }
}

fun main() {
    thread.start()
    Thread.sleep(2000)
    isRunning = false // Stop the thread
}

Without volatile, changes to isRunning may not be visible immediately to the running thread due to caching.


6. Reentrant Locks & Conditional Variables

Reentrant locks allow a thread to re-enter a lock it already holds. Condition variables (newCondition()) help manage threads waiting for specific conditions.

import java.util.concurrent.locks.ReentrantLock

val lock = ReentrantLock()
val condition = lock.newCondition()

fun waitingTask() {
    lock.lock()
    try {
        println("Waiting for condition...")
        condition.await() // Wait until signaled
        println("Condition met, resuming...")
    } finally {
        lock.unlock()
    }
}

fun signalingTask() {
    lock.lock()
    try {
        println("Signaling condition...")
        condition.signal() // Signal waiting threads
    } finally {
        lock.unlock()
    }
}

This setup allows greater flexibility for complex waiting conditions.

7. Missed Signals

Missed signals happen when notify or signal is called before a thread begins waiting. Avoid this by controlling timing, ensuring that waiting threads start before signaling threads.

8. Semaphores

Semaphores control the number of threads that can access a resource at the same time. Semaphore from java.util.concurrent can limit concurrent access.

import java.util.concurrent.Semaphore

val semaphore = Semaphore(3) // Allows 3 threads concurrently

fun accessResource() {
    semaphore.acquire()
    try {
        println("Resource accessed by ${Thread.currentThread().name}")
    } finally {
        semaphore.release()
    }
}

This limits resource access to three threads at any time.

9. Spurious Wakeups

Spurious wakeups occur when a thread unexpectedly wakes up without receiving a notification. To handle these, place wait() calls in a loop that checks the condition repeatedly.

val lock = Object()
var condition = false

fun safeWait() {
    synchronized(lock) {
        while (!condition) {
            lock.wait()
        }
        println("Condition met!")
    }
}

This loop ensures the thread only proceeds if condition is actually met.


10. Atomic Classes

Kotlin provides atomic classes (like AtomicInteger, AtomicReference) that perform atomic operations on variables without needing synchronization.

import java.util.concurrent.atomic.AtomicReference

data class Person(val name: String, val age: Int)

val atomicPerson = AtomicReference(Person("Alice", 25))

fun updatePerson() {
    atomicPerson.updateAndGet { person -> person.copy(age = person.age + 1) }
    println("Updated person: ${atomicPerson.get()}")
}

This is thread-safe and doesn’t require explicit locks.


11. Non-Blocking Synchronizations

Non-blocking synchronization avoids traditional locks by using atomic classes or compareAndSet operations. This allows threads to retry operations without blocking.

val atomicCounter = AtomicInteger(0)

fun incrementCounter() {
    var current: Int
    do {
        current = atomicCounter.get()
    } while (!atomicCounter.compareAndSet(current, current + 1))
    println("Non-blocking increment to: ${atomicCounter.get()}")
}

This pattern, known as "optimistic locking," retries until successful, ensuring thread safety without traditional locks.

Understanding multithreading in Kotlin is key for high-performance application development. Concepts like atomic assignments, synchronization mechanisms, wait/notify, volatile, and non-blocking synchronization can help you design more efficient and safer multithreaded applications. Leveraging Kotlin’s atomic classes and structured concurrency can simplify complex concurrent code, making it easier to manage and debug. Armed with these concepts, you’re ready to build responsive, concurrent Kotlin applications!

Subscribe to Sid Pillai

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe