Multithreading
Run tasks at the same time with Thread, Runnable, synchronized and ExecutorService.
A thread is an independent path of execution inside a program. Every Java program starts with one thread (the one running main), and you can start more so that several tasks run at the same time, for example downloading files while the user keeps working.
Creating threads
Give a Thread a Runnable (often a lambda) describing the work, then call start(). join() waits for a thread to finish.
public class Main {
public static void main(String[] args) throws InterruptedException {
int[] results = new int[2];
Thread evens = new Thread(() -> {
int sum = 0;
for (int i = 0; i <= 100; i += 2) sum += i;
results[0] = sum;
});
Thread odds = new Thread(() -> {
int sum = 0;
for (int i = 1; i <= 100; i += 2) sum += i;
results[1] = sum;
});
evens.start(); // both threads now run at the same time
odds.start();
evens.join(); // wait for both to finish
odds.join();
System.out.println("Even sum: " + results[0]);
System.out.println("Odd sum: " + results[1]);
System.out.println("Total: " + (results[0] + results[1]));
}
}
Even sum: 2550 Odd sum: 2500 Total: 5050
Race conditions and synchronized
When two threads update the same variable, their steps can interleave and updates get lost. This is a race condition. Marking a method synchronized lets only one thread run it at a time.
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) counter.increment();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + counter.get()); // always 200000
}
}
class Counter {
private int count = 0;
synchronized void increment() { // remove "synchronized" and the total comes out wrong
count++;
}
synchronized int get() {
return count;
}
}
Count: 200000
count++ looks like one step but is really three (read, add, write). Without synchronized, two threads can read the same old value and one increment is lost.
ExecutorService: a pool of threads
Creating threads by hand gets messy. An ExecutorService manages a pool of reusable threads; you submit tasks and get back Future objects that hold the results.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main {
static long countPrimes(int from, int to) {
long count = 0;
for (int n = Math.max(2, from); n < to; n++) {
boolean prime = true;
for (int d = 2; (long) d * d <= n; d++) {
if (n % d == 0) { prime = false; break; }
}
if (prime) count++;
}
return count;
}
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Future<Long>> parts = new ArrayList<>();
for (int start = 0; start < 100_000; start += 25_000) {
final int s = start;
parts.add(pool.submit(() -> countPrimes(s, s + 25_000)));
}
long total = 0;
for (Future<Long> f : parts) {
total += f.get(); // waits for that task's result
}
pool.shutdown();
System.out.println("Primes below 100000: " + total);
}
}
Primes below 100000: 9592
Thread-safe tools
Instead of writing synchronized yourself, you can often use classes from java.util.concurrent that are already safe to share between threads:
AtomicInteger,AtomicLong: counters with atomicincrementAndGet().ConcurrentHashMap: a map many threads can update at once.CompletableFuture: chain asynchronous tasks, similar to promises in JavaScript.
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) throws InterruptedException {
AtomicInteger hits = new AtomicInteger();
Thread[] workers = new Thread[4];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) hits.incrementAndGet();
});
workers[i].start();
}
for (Thread w : workers) w.join();
System.out.println("Hits: " + hits.get());
}
}
Hits: 4000