Multithreading

Run more than one thing at once. Learn how to start threads, wait for them, share data safely, and avoid the bugs that make concurrency famously hard.

Learn Multithreading in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

You should be comfortable with interfaces and lambdas (a Runnable is an interface, and you'll write tasks as lambdas) and with static methods and fields . If those feel shaky, revisit the earlier Java lessons first — multithreading is hard enough without fighting the basics.

So far your programs have done one thing at a time , top to bottom. A thread is a separate line of execution — a second worker running through code at the same time as your main code. Running multiple threads at once is called multithreading or concurrency .

💡 Analogy: Think of a kitchen. With one chef (single thread), every order is cooked one after another. Hire more chefs (threads) and several dishes get made at once — but now they share one kitchen (memory). Two chefs reaching for the same knife, or both editing the same order ticket, causes chaos. That shared chaos is exactly what synchronization is for.

Why bother? Concurrency lets you keep an app responsive while it waits on slow things (network, disk), and it lets you use all of a modern CPU's cores. The cost is that shared data becomes dangerous — which is the whole second half of this lesson.

A Runnable is simply a task — an object with one method, run() . You usually write it as a lambda. To run that task on its own thread, hand it to a Thread and call start() .

join() means " wait here until that thread finishes ." Without it, your main method might end before the other threads have done their work.

The thread lifecycle (the states a thread moves through):

You can read a thread's current state with thread.getState() , as the worked example below shows.

The "Main thread keeps going..." line can print before or after the new thread's greeting — the OS decides the timing. Everything after join() is guaranteed to run once the thread is TERMINATED .

Threads in the same program share memory. That's powerful — and dangerous. A race condition happens when two threads read and write the same data at the same time, so the final result depends on luck (who "wins the race").

The classic case is counter++ . It looks like one step, but the CPU does three : read the current value, add one, write it back. Watch two threads collide:

Run that millions of times and you lose thousands of increments. The fix is to make the read-modify-write atomic — indivisible — so no other thread can interleave with it.

The synchronized keyword puts a lock (also called a mutex) around a method or block. Only one thread can hold the lock at a time; others wait their turn. That turns the three-step counter++ into a single, uninterruptible critical section.

A lock has a cost — threads queue up — so synchronize the smallest section that actually touches shared data, not your whole program.

There's a second, sneakier problem besides races: visibility . For speed, each thread may cache a copy of a field. Thread A can set ready = true while thread B keeps reading its own stale false forever.

Marking a field volatile fixes that: every read sees the most recent write from any thread, with no caching. Use it for a simple flag written by one thread and read by others. Note it does not make count++ safe — that still needs synchronized or an atomic, because volatile only guarantees visibility, not atomicity.

Sometimes one thread needs to wait until another is ready . The low-level tools are wait() and notify() , both called while holding a lock. wait() releases the lock and sleeps; notify() wakes one waiting thread. Always wait inside a while loop that re-checks the condition (to guard against spurious wake-ups).

Everything above is the foundation — and it's exactly the code you should avoid writing in real projects. Raw threads, manual locks, and wait() / notify() are powerful but very easy to get subtly wrong (forgotten locks, deadlocks, missed signals).

Modern Java gives you safer, higher-level tools that wrap these primitives correctly:

Time to fly without the scaffolding. The starter below has only a comment outline — you write the logic. Use everything from this lesson: a shared static field, a synchronized method, two threads, start() and join() .

Great work — you've met the hard part of Java head-on. You can now define tasks with Runnable , start threads and wait with join() , read the thread lifecycle, fix race conditions with synchronized , use volatile for visibility, and signal between threads with wait() / notify() . Most importantly, you know why production code reaches for higher-level tools instead.

Next up: Concurrency Utilities — Executors, locks, semaphores, and latches that make all of this safer and easier.

Practice quiz

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

  • They are identical
  • run() spawns a new thread; start() does not
  • start() spawns a new thread; run() executes on the current thread
  • start() blocks until the thread finishes

Answer: start() spawns a new thread; run() executes on the current thread. start() creates a new thread and runs the task there. Calling run() directly just runs it on the current thread — no concurrency.

What does t.join() do?

  • Blocks the caller until t finishes
  • Starts thread t
  • Merges two threads into one
  • Pauses t for one second

Answer: Blocks the caller until t finishes. join() makes the current thread wait until t has terminated before continuing.

What state is a Thread in immediately after 'new Thread(r)' but before start()?

  • RUNNABLE
  • WAITING
  • TERMINATED
  • NEW

Answer: NEW. A freshly created thread is in the NEW state. It becomes RUNNABLE only after start().

Why is 'counter++' on shared state a race condition?

  • It is too slow
  • It is actually three steps (read, add, write) that two threads can interleave
  • It throws an exception
  • It only works on one CPU core

Answer: It is actually three steps (read, add, write) that two threads can interleave. counter++ reads, adds one, and writes back. Two threads can read the same value, so one increment is silently lost.

What does the 'synchronized' keyword provide?

  • Mutual exclusion (and visibility) so one thread runs the block at a time
  • Visibility only
  • Faster execution
  • Automatic thread creation

Answer: Mutual exclusion (and visibility) so one thread runs the block at a time. synchronized takes a lock so only one thread is inside the block/method at a time, making the section atomic.

What does 'volatile' guarantee?

  • Atomic increments
  • Mutual exclusion
  • Visibility — every read sees the latest write, with no stale cache
  • Thread creation

Answer: Visibility — every read sees the latest write, with no stale cache. volatile guarantees visibility but NOT atomicity. count++ on a volatile is still unsafe; use synchronized or an atomic.

When using wait()/notify(), where must you call them?

  • Anywhere in the code
  • While holding the lock (inside a synchronized block) on that object
  • Only from the main thread
  • Only inside a constructor

Answer: While holding the lock (inside a synchronized block) on that object. wait() and notify() must be called while holding the object's monitor lock; wait() releases the lock and sleeps.

Why should you wait() inside a while loop rather than an if?

  • To loop forever
  • Because if is not allowed
  • To speed up the wake-up
  • To re-check the condition and guard against spurious wake-ups

Answer: To re-check the condition and guard against spurious wake-ups. A thread can wake spuriously, so you re-check the condition in a while loop before proceeding.

Which is the preferred high-level tool for a correct, lock-free counter?

  • volatile int
  • AtomicInteger
  • a plain int
  • a new Thread per increment

Answer: AtomicInteger. AtomicInteger.incrementAndGet() uses a hardware compare-and-swap for a correct, lock-free counter.

Why prefer implementing Runnable over extending Thread?

  • Runnable is faster
  • Thread cannot be started
  • Java allows only single inheritance, so a Runnable task can be reused in pools and executors
  • Runnable runs without a thread

Answer: Java allows only single inheritance, so a Runnable task can be reused in pools and executors. extending Thread burns your one superclass slot; a Runnable is just a task you can hand to a Thread or an ExecutorService.