Java Threads and Concurrency

A thread is a path of execution. A Java program starts with one thread (main). With more threads, a program can do many jobs at the same time, like downloading a file while the screen stays active.

Create a thread

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> System.out.println("Hello from " + Thread.currentThread().getName());

        Thread t1 = new Thread(job, "worker-1");
        Thread t2 = new Thread(job, "worker-2");
        t1.start();
        t1.join();             // wait until t1 finishes
        t2.start();
        t2.join();
        System.out.println("Main ends");
    }
}
Output

Hello from worker-1
Hello from worker-2
Main ends

Call start() to run in a new thread. If you call run() directly, the code runs in the current thread, with no new thread. join() waits for a thread to finish.

The race condition problem

When two threads change the same variable, the result can be wrong, because count++ is really three steps (read, add, write).

public class Race {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                count++;                  // not safe
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println("Expected 200000, and the result is correct: " + (count == 200_000));
    }
}
Output

Expected 200000, and the result is correct: false

On a normal computer the answer is almost always not correct, and it is different on every run. That is why this is a dangerous bug.

Fix with synchronized

public class Safe {
    static int count = 0;

    static synchronized void increment() {      // only one thread at a time
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                increment();
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(count);
    }
}
Output

200000

For counters, AtomicInteger is simpler and faster:

import java.util.concurrent.atomic.AtomicInteger;

public class Atomic {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger();
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                count.incrementAndGet();
            }
        };
        Thread a = new Thread(job);
        Thread b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(count.get());
    }
}
Output

200000

ExecutorService: a pool of threads

In real programs you do not create threads by hand. You give tasks to an ExecutorService, which reuses threads.

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 Pool {
    public static void main(String[] args) throws Exception {
        try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
            List<Future<Integer>> results = new ArrayList<>();
            for (int i = 1; i <= 5; i++) {
                int n = i;
                results.add(pool.submit(() -> n * n));      // a Callable returns a value
            }
            for (Future<Integer> f : results) {
                System.out.println(f.get());
            }
        }
    }
}
Output

1
4
9
16
25

Virtual threads (Java 21+)

Virtual threads are very light threads made by the JVM. You can have hundreds of thousands of them, which is great for programs that wait a lot (web calls, databases).

import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class Virtual {
    public static void main(String[] args) {
        AtomicInteger done = new AtomicInteger();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    try { Thread.sleep(100); } catch (InterruptedException e) { }
                    done.incrementAndGet();
                });
            }
        }
        System.out.println("Tasks done: " + done.get());
    }
}
Output

Tasks done: 10000

Thread safety facts

  • Immutable objects (like String and records) are safe to share.
  • Use ConcurrentHashMap, CopyOnWriteArrayList and AtomicInteger instead of locking by hand.
  • Do not forget that ArrayList and HashMap are not thread safe.
Note: Concurrency is a big topic. Start with ExecutorService and immutable data, and use plain synchronized only when you must. Next: Java Pattern Matching and Sealed Classes.

Congratulations! Chapter Finished. Learn more about the similar topics:
Exercises & Assignments
No Content Found.
Interview Questions & Answers
No Content Found.