Home/Learn/Java A–Z/Concurrency Basics

Concurrency Basics

Intermediate
Concurrency

Java concurrency lets multiple threads execute simultaneously. Understanding threads, their lifecycle, and common pitfalls is foundational.

Overview

Java supports multi-threading natively via java.lang.Thread and java.lang.Runnable. Every Java application starts with the main thread; additional threads can be created to run tasks in parallel. Understanding the thread lifecycle (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED), the causes of thread-safety issues (visibility, atomicity, ordering), and the Java Memory Model is essential before diving into higher-level concurrency utilities.

Creating and Starting Threads

Three main ways to create a thread: extend Thread, implement Runnable, or use a lambda. Prefer Runnable/lambda over extending Thread — it separates the task from the execution mechanism.

Thread.start() creates a new OS thread and runs run() on it. Calling run() directly executes on the current thread — a common mistake.

ThreadCreation.java
// 1. Extend Thread (not recommended for modern code)
class MyThread extends Thread {
    @Override public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}
new MyThread().start();

// 2. Implement Runnable (better — separates task from thread)
Runnable task = () -> System.out.println("Task in: "
    + Thread.currentThread().getName());
new Thread(task).start();

// 3. Thread factory with name (Java 19+)
Thread t = Thread.ofPlatform()
    .name("worker-", 0)
    .start(() -> System.out.println("Named thread"));

// Thread state inspection
Thread main = Thread.currentThread();
System.out.println(main.getName());     // main
System.out.println(main.getState());    // RUNNABLE
System.out.println(main.isDaemon());    // false
System.out.println(main.getPriority()); // 5

Thread Lifecycle and Control

Thread states: NEW (created, not started), RUNNABLE (executing or ready), BLOCKED (waiting for monitor), WAITING (waiting indefinitely — Object.wait/join), TIMED_WAITING (waiting with timeout), TERMINATED (finished).

sleep() pauses the current thread for at least the specified duration. join() makes the calling thread wait until the target thread terminates. interrupt() sets the interrupt flag; threads must check it.

ThreadLifecycle.java
Thread worker = new Thread(() -> {
    try {
        System.out.println("Working...");
        Thread.sleep(2000); // TIMED_WAITING
        System.out.println("Done");
    } catch (InterruptedException e) {
        // Interrupt requested — clean up and exit
        System.out.println("Interrupted!");
        Thread.currentThread().interrupt(); // restore flag
    }
});

worker.start();
System.out.println("Worker state: " + worker.getState()); // RUNNABLE

Thread.sleep(500);
worker.interrupt(); // wake it up early

worker.join();      // wait for worker to finish
System.out.println("Worker state: " + worker.getState()); // TERMINATED

Race Conditions and Thread Safety

A race condition occurs when the outcome depends on the unpredictable timing of thread scheduling. The classic example: two threads incrementing a shared counter without synchronization.

Three root causes of thread-safety problems: 1. Atomicity — read-modify-write operations are not atomic. 2. Visibility — changes by one thread may not be seen by others (CPU caches). 3. Ordering — the compiler and CPU may reorder instructions.

RaceCondition.java
// Unsafe counter — race condition
public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // NOT atomic: read → add 1 → write (3 steps)
    }

    public int get() { return count; }
}

// Demonstrate the problem
UnsafeCounter counter = new UnsafeCounter();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    threads.add(new Thread(counter::increment));
}
threads.forEach(Thread::start);
for (Thread t : threads) t.join();

// Expected: 1000 — Actual: usually less due to lost updates
System.out.println("Count: " + counter.get());

// Solutions: synchronized, AtomicInteger, LongAdder
// (covered in next topics)

Interactive Visualization

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

Key Points to Remember

  • Prefer Runnable/lambda over extending Thread — separates the task from execution.
  • Call start(), not run(), to launch a new thread.
  • Thread states: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED.
  • interrupt() sets the interrupt flag; catch InterruptedException and restore the flag.
  • Race conditions arise from atomicity, visibility, and ordering violations.

Practice Concurrency Basics in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between start() and run() in Thread?

EasyTCS
2

What are the possible states of a Java thread?

EasyAmazon
3

What is a race condition? Give an example.

EasyGoogle
4

What happens if you call interrupt() on a sleeping thread?

MediumMicrosoft
5

Why should you restore the interrupt flag in a catch(InterruptedException) block?

MediumOracle

Ask Aria about Concurrency Basics

Your personal AI tutor — ask anything about this concept