Java Threads
A program normally does one thing after another. A thread is a separate path of execution, so a program with several threads can do several things at the same time: download a file while the user keeps typing, or split a big calculation into parts. Every Java program starts with one thread, the main thread, which runs main.
Starting a thread
Create a Thread with the work to do (a Runnable, usually written as a lambda), then call start(). join() waits until a thread has finished:
public class Main {
public static void main(String[] args) throws InterruptedException {
int[] partial = new int[2];
Thread first = new Thread(() -> {
for (int i = 1; i <= 500; i++) {
partial[0] += i;
}
});
Thread second = new Thread(() -> {
for (int i = 501; i <= 1000; i++) {
partial[1] += i;
}
});
first.start();
second.start();
first.join();
second.join();
System.out.println("Sum of 1 to 1000: " + (partial[0] + partial[1]));
}
}Sum of 1 to 1000: 500500
The two threads add up their halves at the same time; main waits for both and combines the results. Each thread writes only to its own element of the array, so they never get in each other's way. join can throw an InterruptedException, a checked exception, which is why main declares it.
Race conditions and synchronized
When threads change the same variable, results can go wrong: count++ is really "read, add one, write", and two threads can read the same old value. This is called a race condition. Marking a method synchronized lets only one thread run it at a time:
class Counter {
private int count = 0;
synchronized void increment() {
count++;
}
int get() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Thread[] workers = new Thread[4];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Thread(() -> {
for (int j = 0; j < 10_000; j++) {
c.increment();
}
});
workers[i].start();
}
for (Thread w : workers) {
w.join();
}
System.out.println("Count: " + c.get());
}
}Count: 40000
With synchronized the count is always exactly 40,000. Without it, the result can be smaller and different on every run. Java also offers ready-made safe types such as AtomicInteger in java.util.concurrent.atomic.
ExecutorService: a pool of threads
Creating threads by hand gets messy. An ExecutorService manages a pool of threads for you. You submit tasks, and each task returns a Future that later gives its result:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
static long countPrimes(int from, int to) {
long count = 0;
for (int n = Math.max(from, 2); n < to; n++) {
boolean prime = true;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
prime = false;
break;
}
}
if (prime) {
count++;
}
}
return count;
}
public static void main(String[] args) throws Exception {
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
List<Future<Long>> parts = new ArrayList<>();
for (int start = 0; start < 20_000; start += 5_000) {
int s = start;
parts.add(pool.submit(() -> countPrimes(s, s + 5_000)));
}
long total = 0;
for (Future<Long> f : parts) {
total += f.get();
}
System.out.println("Primes below 20000: " + total);
}
}
}Primes below 20000: 2262
f.get() waits for that task's result. The try-with-resources block (see Throw and Throws) shuts the pool down at the end.
Virtual threads
Java 21 added virtual threads: very light threads, so a program can run thousands of them, for example one per network request. They are created with Thread.ofVirtual() or Executors.newVirtualThreadPerTaskExecutor():
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger done = new AtomicInteger();
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
ex.submit(() -> {
Thread.sleep(10);
return done.incrementAndGet();
});
}
}
System.out.println("Finished tasks: " + done.get());
}
}Finished tasks: 1000
All 1,000 tasks sleep at the same time, so the program finishes in a fraction of a second instead of ten seconds.
Practice exercises
- Start three threads that each compute the sum of a different third of 1 to 3,000, and combine the results.
- Remove
synchronizedfrom the counter, run it several times and compare the results. - Use an
ExecutorServiceto compute the factorials of 10, 15 and 20 in parallel.