Kotlin threads, AtomicInteger, and an executor
Start threads that update a shared counter safely and split a sum across a thread pool.
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()
}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
import java.util.concurrent.Callableimport java.util.concurrent.Executorsimport java.util.concurrent.atomic.AtomicIntegerimport 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()}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
| Step / line | What happens | State after step | Output 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.