Threads
In short: Concurrent threads of execution within a Java program — allow several tasks to run seemingly at the same time, instead of strictly one after another.
In more detail: A new thread starts either via a custom Thread subclass, or, more commonly, via the Runnable interface with new Thread(runnable).start(). If several threads access the same mutable state at the same time, race conditions loom — the synchronized keyword protects critical sections of code from this.
In Depth
// Via Runnable + lambda (more common today than a custom Thread subclass):
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i);
}
};
Thread t = new Thread(task);
t.start(); // starts an ACTUAL new thread of execution, do NOT call t.run()!
// Shared state needs protection from race conditions:
class Counter {
private int value = 0;
synchronized void increment() { value++; } // only one thread in here at a time
}A common beginner mistake: calling t.run() instead of t.start() — run() simply executes the code in the CURRENT thread like a normal method, with no new thread started at all; only start() actually creates a new, parallel-running thread of execution and calls run() within it. Since several threads can access the same memory, race conditions loom: if two threads read AND write the same field at the same time, the final result can depend on the random timing order. The synchronized keyword ensures that only ONE thread at a time is allowed to enter a critical method/block, while all others wait.
For more complex concurrency, Java has offered the java.util.concurrent package since version 5, with higher-level tools like ExecutorService (manages a pool of reusable threads instead of creating a new one for every task), and since Java 21 additionally very lightweight “virtual threads”, which enable thousands of concurrent tasks without each needing an expensive genuine operating system thread.
See also: Lambda, Anonymous Classes