Java Tutorial · Advanced Java · Lesson 36 of 37

Java Threads and Concurrency

Run work in parallel with executors, virtual threads and thread-safe counters.

Threads and executors

A thread runs code concurrently. Instead of creating threads by hand, submit tasks to an ExecutorService and get results back as Futures:

Example 1
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 {
    public static void main(String[] args) throws Exception {
        try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> results = new ArrayList<>();
            for (String city : List.of("London", "Tokyo", "Lima")) {
                results.add(pool.submit(() -> {
                    Thread.sleep(100);   // pretend to call a weather API
                    return city + ": sunny";
                }));
            }
            for (Future<String> f : results) {
                System.out.println(f.get());
            }
        }
    }
}

Output

London: sunny
Tokyo: sunny
Lima: sunny

Run this on your computer — see “Get Started” for setup.

Virtual threads (Java 21)

Virtual threads are lightweight threads managed by the JVM — you can run thousands of blocking tasks (HTTP calls, database queries) without a huge thread pool. Executors.newVirtualThreadPerTaskExecutor() gives you one per task.

Thread safety

When threads share data, unsynchronised updates get lost. Use AtomicInteger, synchronized, locks, or concurrent collections like ConcurrentHashMap:

Example 2
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 safe = new AtomicInteger();
        int[] unsafe = {0};
        try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
            for (int i = 0; i < 4; i++) {
                pool.submit(() -> {
                    for (int j = 0; j < 10_000; j++) {
                        safe.incrementAndGet();
                        unsafe[0]++;   // race condition!
                    }
                });
            }
        }
        System.out.println("atomic: " + safe.get());
        System.out.println("unsafe is 40000? " + (unsafe[0] == 40_000) + " (usually false)");
    }
}

Output

atomic: 40000
unsafe is 40000? false (usually false)

Run this on your computer — see “Get Started” for setup.

Test yourself

Which class gives you a thread-safe integer counter without locks?

  1. Integer
  2. AtomicInteger
  3. volatile int
  4. SafeInt
Show answer

Answer: AtomicInteger

Practice next