Concurrency
Prerequisites: Closures in depth, Using Java libraries
Groovy runs on the JVM, so every Java concurrency tool works — and
closures make them concise. Groovy 6 adds its own async/await on
top (currently marked incubating, meaning the API may still change).
Threads and executors
import java.util.concurrent.*
def pool = Executors.newFixedThreadPool(4)
def futures = (1..5).collect { n ->
pool.submit({ -> Thread.sleep(50); n * n } as Callable)
}
println futures*.get()
pool.shutdown()
println pool.awaitTermination(5, TimeUnit.SECONDS)
[1, 4, 9, 16, 25]
true
submit returns a Future; get() waits for the result. Results come
back in submission order even though the tasks ran in parallel.
Virtual threads (Java 21+)
import java.util.concurrent.Executors
def start = System.currentTimeMillis()
def results = Executors.newVirtualThreadPerTaskExecutor().withCloseable { ex ->
(1..1000).collect { i -> ex.submit { Thread.sleep(100); i } }*.get()
}
def seconds = (System.currentTimeMillis() - start) / 1000
println "${results.size()} tasks, sum ${results.sum()}"
println "finished in well under 100 s: ${seconds < 10}"
1000 tasks, sum 500500
finished in well under 100 s: true
A thousand tasks that each sleep for 100 ms finish together in a fraction of a second, because virtual threads are cheap enough to create one per task. They're ideal for I/O-bound work like HTTP calls and database queries.
CompletableFuture
import java.util.concurrent.CompletableFuture
def price = CompletableFuture.supplyAsync { 100 }
def rate = CompletableFuture.supplyAsync { 0.2 }
def total = price.thenCombine(rate) { p, r -> p * (1 + r) }
println total.get()
def failing = CompletableFuture.supplyAsync { throw new IllegalStateException('service down') }
println failing.exceptionally { e -> "fallback (${e.cause.message})" }.get()
120.0
fallback (service down)
async and await (Groovy 6)
def fetch(String name, long ms) { Thread.sleep(ms); "$name data" }
def users = async { fetch('users', 200) }
def orders = async { fetch('orders', 100) }
def (u, o) = await(users, orders)
println "$u + $o"
def total = await async {
def a = await async { 20 }
def b = await async { 22 }
a + b
}
println total
def fibonacci = async {
long a = 0, b = 1
while (true) {
yield return a
(a, b) = [b, a + b]
}
}
println fibonacci.take(10).collect()
users data + orders data
42
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
async { }starts a task (on a virtual thread on Java 21+).awaitwaits for one or several tasks and returns their results — the code reads top to bottom, with ordinarytry/catch.yield returninsideasyncmakes a lazy generator.
Shared state
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.ConcurrentHashMap
def counter = new AtomicInteger()
def unsafe = 0
def words = new ConcurrentHashMap<String, Integer>()
def threads = (1..8).collect { t ->
Thread.start {
1000.times {
counter.incrementAndGet()
unsafe++
words.merge(t % 2 ? 'odd' : 'even', 1, Integer::sum)
}
}
}
threads*.join()
println counter.get()
println words.sort()
// unsafe is often less than 8000: concurrent ++ loses updates
8000
[even:4000, odd:4000]
The atomic counter and concurrent map are always correct. The plain
variable unsafe may come out right on one run and lose updates on the
next — ++ isn't atomic, even though closures make sharing it easy. Share
immutable data where you can, and use java.util.concurrent types for
anything mutable.
Parallel collections
Groovy 6 also adds parallel versions of collection methods for
CPU-bound work, such as collectParallel, findAllParallel, and
eachParallel, plus agents, actors, and dataflow variables in the new
groovy.concurrent package (the successor to the retired GPars
library).