Java
Java Threads
›Short versionOpen if you only have a minute.
- A thread is another sequence of execution in the same process. Threads share the heap.
- Put the work in run(). Call start() to schedule it. start() once per Thread object.
- Calling run() yourself runs the body on the current thread. The Thread stays NEW.
- A second start() throws IllegalThreadStateException.
- A shared amount++ can lose updates: both threads read the same value and both write the same successor. That is a race.
- join() waits until that thread finishes. Print the counter after the joins if you want the finished number.
main is already a . Creating another one means there are two sequences, both able to touch the same fields. The method you write is . The call that actually creates the second sequence is . Those are different methods, and calling the first when you meant the second is the first bug this page is about. The second is a shared counter.
On this page
Task and worker
Runnable plus Thread, or subclass Thread.
start vs run
Schedule once; never call run for concurrency.
Shared counter
See a race, then wait with join.
Scheduling the worker
- Put task logic in run(); call start() exactly once per Thread.
- Prefer Runnable so your domain type keeps its superclass slot.
- After run returns, the Thread is TERMINATED — create a new Thread for a repeat run.
Observing shared work
- amount++ is read, add, write — three steps another thread can interleave.
- join() before reading a result the workers wrote.
- Fixing lost updates needs synchronization or atomics; this page is about seeing the bug.
start() vs run()
| Call | Which thread runs the body | Thread state after |
|---|---|---|
| run() | Current thread (often main) | Still NEW if start() never ran |
| start() | New worker thread | Moves NEW to runnable, then TERMINATED when run ends |
| start() again | N/A | IllegalThreadStateException |
Read the diagram definition
sequenceDiagram
participant Main
participant T1 as Thread-1
participant T2 as Thread-2
participant Counter as amount
Main->>T1: start()
Main->>T2: start()
T1->>Counter: read / add / write
T2->>Counter: read / add / write (race)
Main->>T1: join()
Main->>T2: join()
Main->>Counter: println (may be less than expected)A task, then a worker
Two shapes create the worker. Subclass Thread and override run, or implement and pass the instance to a Thread constructor. The second shape leaves your class free to extend something that is actually in the domain. The Thread is the worker. The Runnable is the work.
class Cook implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " is cooking");
}
}
Thread worker = new Thread(new Cook());
worker.start();After new Thread, the object is in the NEW state. Nothing concurrent has happened. start is what moves it to runnable and asks for run to be entered on the new sequence. While it runs it is runnable, or blocked waiting for a lock, or waiting. When run returns, the state is TERMINATED. You do not restart it.
Thread worker = new Thread(() ->
System.out.println(Thread.currentThread().getName())
);
worker.run(); // prints "main". The worker is still NEW.
worker.start(); // prints a name like "Thread-0"
// worker.start(); // IllegalThreadStateExceptionThe lambda ran both times. The first call was a method call on main. The second was a scheduled thread. Thread.currentThread().getName() is the witness. If the name is main, start never happened.
A counter both of them update
Two cooks can chop at once because the vegetables are separate. Two threads that both execute amount++ on one field are reaching for the same knife, one step at a time. The read of amount, the add, and the write are three steps. Interleave them and one update is lost. That lost update is a . This page stops at seeing it. Locks, atomic integers, and the memory model are the next subject, and they exist because this counter is already wrong.
public class CounterDemo {
private static int amount = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
amount++;
}
};
Thread first = new Thread(increment);
Thread second = new Thread(increment);
first.start();
second.start();
first.join();
second.join();
System.out.println(amount); // can be less than 2000
}
} matters for the measurement. Without it, main can print while the loops are still running, and a small number means "you looked too soon." With both joins, both loops have returned, and a number under 2000 means increments were overwritten. It is not guaranteed to be short on every run. A race that shows up only sometimes is still a race. The repair is to make the increment a single update, with a synchronized method or an AtomicInteger, which is where the concurrency notes pick this example up.
The log line inside run() says the thread name is main. You expected a second thread. What call did the code actually make?
It called run() on the Thread object. That is a normal method call, so the body executes on main and the Thread remains NEW. start() is the call that schedules run() on a new sequence. After that, Thread.currentThread().getName() is the worker’s name.
Principal-engineer rule
Never call run() when you mean concurrency — only start() creates a second sequence. Treat every shared mutable field as a race until you have a deliberate synchronization or atomic story, and always join before you treat worker output as finished.
Good default: implement Runnable, wrap with new Thread(task), call start() once, join() before reading shared results, and use an executor instead of raw Thread when you outgrow one-off workers.