Java Multithreading Basics: Threads, Runnable and Safe Counters with Examples
Build two Java thread examples, trace a lost counter update exactly, and learn when to use synchronized, AtomicInteger, volatile or private state.
KnowledgeGate Team
Exam prep & CS education

A Java program can create threads in a few lines, yet output may change between runs and a shared counter may silently lose updates. Both surprises come from one place: the scheduler is free to interleave threads however it likes, and value++ is three separate steps rather than one.
What multithreading means in one Java process
A thread is one path of execution inside a process. Java starts with the main thread, which can start workers that make progress concurrently and share objects on the same heap. Concurrency does not guarantee simultaneous CPU execution.
The official Java SE 21 Thread documentation describes multiple concurrent threads of execution. For the process-versus-thread model behind the Java objects, revise Threads and Process Creation MCQs.
Picture main starting workers alpha and beta. Each prints three steps; main waits and prints main:done.
Three facts remain true in every correct run:
alphakeeps its own three steps in program order.betakeeps its own three steps in program order.main:doneappears last becausemainjoins both workers.
The scheduler may still mix their lines differently on each run.
Create two threads with Runnable, start() and join()
The first program starts two workers and waits for both:
public class ThreadSteps {
static class StepTask implements Runnable {
private final String label;
StepTask(String label) {
this.label = label;
}
@Override
public void run() {
for (int step = 1; step <= 3; step++) {
System.out.println(label + ":" + step);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread alpha = new Thread(new StepTask("A"), "alpha");
Thread beta = new Thread(new StepTask("B"), "beta");
alpha.start();
beta.start();
alpha.join();
beta.join();
System.out.println("main:done");
}
}One valid trace is:
A:1
B:1
A:2
A:3
B:2
B:3
main:doneThe guarantees are A:1 < A:2 < A:3, B:1 < B:2 < B:3, and both workers finishing before main:done. No rule orders an A line against a B line.
Extending Thread is valid: class MyThread extends Thread { public void run() { ... } }. A Runnable separates the task from the thread that executes it, and it leaves the class free to extend something else, since Java allows only one superclass.

Read the lifecycle without treating it as a fixed schedule
Java defines six Thread.State values, and each one names a different reason the thread is not running your code at this instant:
NEW: theThreadobject exists, butstart()has not been called yet.RUNNABLE: eligible to run. It may be on a CPU or queued for one, and Java does not distinguish the two.BLOCKED: waiting to acquire a monitor lock another thread holds. This is where threads queue at asynchronizedmethod.WAITING: waiting indefinitely for another thread to act, afterwait()or ajoin()with no timeout.TIMED_WAITING: the same wait with a deadline, afterThread.sleep(ms),wait(ms)orjoin(ms).TERMINATED:run()has returned or thrown.
In this program alpha.getState() is NEW before start() and TERMINATED after join() returns. Anywhere between those two points the state is a sample rather than a fact: it can change before the line that prints it runs. Meanwhile main itself sits in WAITING for as long as alpha.join() has not returned.
The API contracts are precise:
start()schedules the new thread to execute itsrun()method.join()waits for that thread to terminate.Thread.sleep(ms)asks the currently executing thread to pause for about that many milliseconds, subject to timer and scheduler precision.
Calling alpha.run() directly is an ordinary call on the current thread, not concurrent work. sleep() is not an ordering tool either.
Worked example: three workers updating one counter
Now let three workers share an unsafe counter:
public class CounterDemo {
static final int WORKERS = 3;
static final int REPEATS = 100_000;
static class UnsafeCounter {
int value = 0;
void increment() {
value++;
}
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread[] workers = new Thread[WORKERS];
for (int i = 0; i < WORKERS; i++) {
workers[i] = new Thread(() -> {
for (int repeat = 0; repeat < REPEATS; repeat++) {
counter.increment();
}
}, "worker-" + (i + 1));
}
for (Thread worker : workers) worker.start();
for (Thread worker : workers) worker.join();
System.out.println(counter.value);
}
}The requested total is:
3 × 100,000 = 300,000
The unsafe result is nondeterministic. It may be below 300000 or occasionally equal it. No wrong output is guaranteed.
Suppose value is 9. Worker 1 reads 9 and computes 10. Before its write, worker 2 also reads 9 and computes 10. Both write 10. Two increments leave 10, although serial execution reaches 11. Conceptually, value++ is read, add 1, write.
Repair the class by changing only its method:
synchronized void increment() {
value++;
}The monitor allows one worker at a time through the increment, so correctness requires each increment to finish before the next begins. After all three joins, the output must be 300000. Synchronization in Java applies the same principle to monitor identity, lock scope, wait()/notifyAll(), and deadlock prevention.
Choose the smallest correct sharing tool
Choose by the operation you must protect:
Use
synchronizedwhen several statements must form one critical section.Use
AtomicInteger.incrementAndGet()for one atomic counter operation.Use
volatilewhen visibility matters, but remember thatvolatile int value; value++;is still a compound, non-atomic update.
For the atomic variant, use AtomicInteger value = new AtomicInteger(0), call value.incrementAndGet(), and print value.get() after joining. Three workers completing 100,000 calls each produce exactly 300000.
First ask whether sharing is needed at all. Where the problem allows it, give each worker a private subtotal and let main add the three subtotals after the joins: no lock, no atomic class, and still 300000.
Common Java multithreading mistakes and repairs
Pair each mistake with the matching repair:
Calling
alpha.run()keeps the work on the caller. Usealpha.start().Starting the same
Threadobject twice throwsIllegalThreadStateException. Create a new thread object for new work.Omitting
join()can letmaincontinue before a result is ready. Join the workers whose completion is required.Using
Thread.sleep(100)does not prove another thread has finished. Usejoin()or the suitable coordination primitive.Marking a counter
volatiledoes not makecount++indivisible. UsesynchronizedorAtomicIntegerhere.
Do not silently swallow InterruptedException. Propagate it when appropriate, or restore the flag with Thread.currentThread().interrupt(). Keep one lock order: worker 1 taking lockA then lockB while worker 2 takes them in reverse can deadlock. Avoid deprecated stop(), suspend() and resume().
How tests and interviews assess thread basics
Four question shapes cover most of what gets asked:
Given a two-thread program, which printed lines are guaranteed. Answer with per-thread program order plus whatever
join()orders, and nothing else.What
alpha.run()prints compared withalpha.start(). Both print the same three lines, but onlystart()prints them from a second thread.Why a counter that should read
300000reads less. Name the read, add and write window rather than saying the threads clashed.Which of
synchronized,volatileandAtomicIntegerrepairs a given snippet. Match the tool to the operation: a compound update needssynchronizedorAtomicInteger, andvolatilealone never suffices.
Try these exact exercises:
Change the labels to X and Y and print two steps each. The guarantees are
X:1 < X:2,Y:1 < Y:2, and all four worker lines beforemain:done.Use three safe workers with
2,000increments each. Check3 × 2,000 = 6,000.Start at
9and trace two unsynchronised increments. Both workers can read9, both compute10, and both can write10, instead of the serial result11.
For practice across the rest of the language, and for the coding-round versions of these questions, work through our Coding & DSA courses.
Java multithreading basics, the short version
Put work in run(), start it with start(), and use join() when completion is required. Expect varying schedules, minimise shared mutable state, and protect compound updates.
Three safe workers times 100,000 increments produce 300000; the unsafe version has no promised result. Run both programs, make the three exercise changes, then keep going with the Java Course.
Keep learning

Static and Final in Java: Shared State, Constants and Override Rules with Runnable Examples
Learn what static and final control in Java by tracing a shared counter, class initialisation, final references, constants and inheritance errors.

Polymorphism in Java: Overloading, Overriding and Runnable Examples
Learn how Java chooses overloaded methods at compile time and overridden methods at runtime. Trace two complete programs, common traps and practice targets.

Packages and Access Modifiers in Java: Visibility Rules with Runnable Examples
Learn how Java package boundaries change member visibility. Compile a complete example covering public, protected, package-private and private access.

OOP Basics in Java: Classes, Objects and the Four Pillars with Runnable Examples
Connect Java's OOP syntax to design decisions with a complete Account example. Trace two objects through validation, overriding and runtime dispatch to exact output.