Kotlin threads, AtomicInteger, and an executor

Start threads that update a shared counter safely and split a sum across a thread pool.

Kotlin
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread

fun main() {
    val counter = AtomicInteger(0)
    val threads = List(4) {
        thread {
            repeat(50_000) { counter.incrementAndGet() }
        }
    }
    threads.forEach { it.join() }
    println("counter = ${counter.get()}")

    val pool = Executors.newFixedThreadPool(4)
    val futures = (1..4).map { part ->
        pool.submit(Callable { ((part - 1) * 250 + 1..part * 250).sumOf { it.toLong() } })
    }
    println("sum = ${futures.sumOf { it.get() }}")
    pool.shutdown()
}
Output
counter = 200000
sum = 500500

Explanation

Kotlin can use JVM threads directly. thread { ... } from kotlin.concurrent starts a new thread, and join waits for it. When several threads change the same variable, plain increments can be lost, because count++ is a read, an add, and a write. AtomicInteger.incrementAndGet performs the whole update as one atomic step, so four threads adding 50000 each always end at 200000.

An ExecutorService manages a pool of worker threads. submit(Callable) runs a task and returns a Future, whose get waits for the result. Splitting a sum into four ranges that do not overlap means the tasks share nothing mutable, so no locks are needed. shutdown lets the pool finish and release its threads. Coroutines, from the kotlinx.coroutines library, are a lighter alternative for waiting work.

Explore the dry run
Guided walkthroughReady
1import java.util.concurrent.Callable
2import java.util.concurrent.Executors
3import java.util.concurrent.atomic.AtomicInteger
4import kotlin.concurrent.thread
5
6fun main() {
7 val counter = AtomicInteger(0)
8 val threads = List(4) {
9 thread {
10 repeat(50_000) { counter.incrementAndGet() }
11 }
12 }
13 threads.forEach { it.join() }
14 println("counter = ${counter.get()}")
15
16 val pool = Executors.newFixedThreadPool(4)
17 val futures = (1..4).map { part ->
18 pool.submit(Callable { ((part - 1) * 250 + 1..part * 250).sumOf { it.toLong() } })
19 }
20 println("sum = ${futures.sumOf { it.get() }}")
21 pool.shutdown()
22}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
An integer whose updates are atomic.—
—
Each call starts a new thread that runs the block.—
—
Every increment is atomic, so no update is lost.—
—
Wait for all four threads to finish before reading the counter.—
—
4 threads times 50000 increments.counter = 200000
counter = 200000
A pool of four worker threads.counter = 200000
counter = 200000
Each task sums its own range of 250 numbers and returns the result.counter = 200000
counter = 200000
get waits for each task. The four partial sums add up to n(n+1)/2 for n = 1000.counter = 200000
counter = 200000
sum = 500500

Cost and common mistakes

O(n) total work; the ideal elapsed time is O(n / p) for p cores.

  • Forgetting pool.shutdown() keeps non-daemon threads alive, so the program may never exit.
  • A plain Int shared by threads and changed with ++ is a data race. Use AtomicInteger, a lock, or avoid sharing.
  • Starting a thread has a cost. For many small tasks, a pool is faster than one thread per task.

Try it yourself

What would happen with a plain var counter instead?

Reveal answer and reasoning

The final value would usually be less than 200000, and different on every run, because increments from different threads overwrite each other.

Further reading