Home
11Multithreaded Programming
11

Topic 11 of 13 · Lectures 19–21

Multithreaded Programming

Running several tasks at once with threads: creating and starting them, the thread life cycle, race conditions and synchronization, and coordinating threads with wait and notify.

← → move between slides · F full screen · M slide list

Before we start

Why this matters

Your music keeps playing while you scroll, download and type. How can one program do several things at the same time?

Each activity runs in its own thread: an independent path of execution inside the same program. Threads make programs responsive and let them use every CPU core.

But threads that share data can trip over each other. You will create threads, then break a counter on purpose, and fix it with synchronization.

Outline · Lectures 19–21

Topics in this chapter

Select a topic to jump to the slide that explains it.

  1. Processes and threads
  2. Thread life cycle
  3. Creating threads: Thread and Runnable
  4. Synchronization
  5. Interthread communication: wait and notify

Concept 1 of 6

Processes and threads

Threads of one process share the heap but each has its own stack.

Concept 1 of 6

Processes and threads (cont.)

  • A process is a running program with its own memory. A thread is a path of execution inside a process.
  • Threads share the heap (objects, static fields), so they communicate cheaply — and can interfere.
  • Every Java program starts with the main thread; the JVM also runs background threads such as the garbage collector.
  • On a multi-core CPU threads can truly run in parallel; on one core they take turns (time-slicing).

Concept 2 of 6

The thread life cycle

The scheduler picks which Runnable thread becomes Running. stop(), suspend() and resume() are deprecated; avoid them in new code.

The thread life cycle

StateMeaning
NewCreated with new Thread(...), not started yet.
RunnableAfter start(): ready, waiting for the CPU.
RunningThe scheduler gave it the CPU; run() is executing.
BlockedIdle after sleep(), wait() or suspend(); back to Runnable on notify(), resume() or time-out.
Deadrun() finished, or the thread was stopped.

Code example

Creating threads

class Worker extends Thread {                 // way 1
    Worker(String name) { super(name); }
    public void run() {
        for (int i = 1; i <= 3; i++)
            System.out.println(getName() + " step " + i);
    }
}
class Task implements Runnable {              // way 2 (preferred)
    public void run() {
        System.out.println("Task in " + Thread.currentThread().getName());
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread w = new Worker("W");
    Thread t = new Thread(new Task(), "T");
    Thread l = new Thread(() -> System.out.println("Lambda"), "L");  // way 3
    w.start(); t.start(); l.start();

Code example

Creating threads (cont.)

    w.join(); t.join(); l.join();
    System.out.println("main ends");
}
One possible output
W step 1
Task in T
W step 2
Lambda
W step 3
main ends
  • Put the thread’s work in run(), but call start() — start creates the new thread, which then calls run.
  • Implementing Runnable keeps your class free to extend something else.
  • The order of lines from different threads can change on every run.

Step through the code

Trace: two threads, two stacks

Every thread has its own call stack. Watch the states change from NEW to RUNNABLE to TERMINATED, and main WAIT in join().

Step through the code

Trace: two threads, two stacks (cont.)

Interactive

Two threads, many outputs

Code example

sleep, join and other Thread methods

Thread timer = new Thread(() -> {
    for (int s = 3; s >= 1; s--) {
        System.out.println(s + "...");
        try { Thread.sleep(1000); }           // pause 1 s
        catch (InterruptedException e) { return; }
    }
    System.out.println("Go!");
});
timer.start();
System.out.println("Waiting for the countdown");
timer.join();                                 // wait until timer ends
System.out.println("Race started, alive? " + timer.isAlive());
Output (the first two lines may swap)
Waiting for the countdown
3...
2...
1...
Go!
Race started, alive? false

Code example

sleep, join and other Thread methods (cont.)

MethodPurpose
Thread.sleep(ms)Pause the current thread (TIMED_WAITING).
t.join()Wait for thread t to finish.
t.isAlive(), getName(), setPriority(1–10)Query or tune a thread (priority is only a hint).
t.interrupt()Ask a sleeping or waiting thread to stop: it gets InterruptedException.

Concept 3 of 6

Race conditions

class Counter {
    private int count = 0;
    void increment() { count++; }   // NOT atomic
    int get() { return count; }
}
Counter c = new Counter();
Runnable job = () -> { for (int i = 0; i < 100_000; i++) c.increment(); };
Thread a = new Thread(job), b = new Thread(job);
a.start(); b.start(); a.join(); b.join();
System.out.println(c.get());   // expected 200000
Typical output — different every run
137462
  • count++ is three steps: read count, add 1, write it back.
  • If both threads read the same old value, one increment is lost.
  • A bug whose result depends on timing is a race condition. It may pass a hundred tests and fail in the exam demo.

Step through the code

Trace: how count++ loses an update

count++ is read, add, write. Compare the two schedules: in the interleaved one, both threads read 0.

Step through the code

Trace: how count++ loses an update (cont.)

Interactive

Race-condition lab

Concept 4 of 6

Synchronization

class Counter {
    private int count = 0;
    synchronized void increment() { count++; }   // method lock on this
    synchronized int get() { return count; }
}

class Bank {
    private final Object lock = new Object();
    private double balance;
    void deposit(double amt) {
        synchronized (lock) {                        // block lock
            balance += amt;
        }
    }
}
  • Every object has a monitor lock. Only one thread at a time can hold it.
  • A synchronized method locks this (a static synchronized method locks the class).
  • Other threads trying to enter become BLOCKED until the lock is released.
  • Keep synchronized sections short; for simple counters AtomicInteger is an alternative.

Step through the code

Trace: a lock makes B wait

With synchronized, B is BLOCKED while A holds the lock, so it can never read a stale value.

Step through the code

Trace: a lock makes B wait (cont.)

Concept 5 of 6

Interthread communication: wait and notify

class Buffer {
    private final Queue<Integer> q = new LinkedList<>();
    private final int CAP = 3;

    synchronized void put(int x) throws InterruptedException {
        while (q.size() == CAP) wait();   // release lock and sleep
        q.add(x);
        notifyAll();                      // wake waiting takers
    }
    synchronized int take() throws InterruptedException {
        while (q.isEmpty()) wait();
        int x = q.remove();

Concept 5 of 6

Interthread communication: wait and notify (cont.)

        notifyAll();                      // wake waiting putters
        return x;
    }
}
  • wait() releases the lock and waits until another thread calls notify()/notifyAll() on the same object.
  • All three may only be called while holding that object’s lock (inside synchronized).
  • Always wait in a while loop: re-check the condition after waking up.
  • This is the classic producer–consumer pattern.

Interactive

Producer–consumer

Concept 6 of 6

Deadlock

Each thread holds one lock and waits forever for the other.

Deadlock

// Thread 1                     // Thread 2
synchronized (a) {              synchronized (b) {
    synchronized (b) { ... }        synchronized (a) { ... }
}                               }
  • A deadlock happens when threads wait for each other in a cycle; none can continue.
  • Prevent it by always taking locks in the same order, holding locks briefly, or using timeouts (tryLock).

Worked example 1 of 3

Worked example 1

What is wrong? Thread t = new Thread(task); t.run();
Show the solution
run() executes the task in the current thread like a normal method call. Use t.start() to create a new thread.

Worked example 2 of 3

Worked example 2

Two threads each call synchronized void inc() 1000 times on the same object. What is the final count? What if each thread uses its own object?
Show the solution
Same object: always 2000. Different objects: each counter reaches 1000; the locks are different but so is the data, so there is no race.

Worked example 3 of 3

Worked example 3

Why should wait() be called in a while loop rather than an if?
Show the solution
A thread can wake up when the condition is still false (another thread got there first, or a spurious wake-up). The while loop re-checks before continuing.

Practice

Practice (1 of 2)

Try each one on paper first, then check your answer.

1. Give two ways to create a thread and one reason to prefer Runnable.
Show the answer
Extend Thread or implement Runnable (or use a lambda). Runnable lets the class extend another class and separates the task from the thread.
2. Which state is a thread in while inside Thread.sleep(500)?
Show the answer
TIMED_WAITING.
3. Write code that starts 3 threads printing their names and waits for all before printing “done”.
Show the answer
Thread[] ts = new Thread[3];
for (int i = 0; i < 3; i++) {
    ts[i] = new Thread(() -> System.out.println(Thread.currentThread().getName()), "T" + i);
    ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println("done");

Practice

Practice (2 of 2)

Try each one on paper first, then check your answer.

4. Why is count++ not thread-safe?
Show the answer
It is a read, an add and a write; another thread can run between them and one update is lost.
5. What is the difference between sleep() and wait()?
Show the answer
sleep pauses without releasing any lock and is called on Thread; wait must hold the object’s lock, releases it, and waits for notify.
6. State one rule that prevents deadlock.
Show the answer
Acquire locks in a fixed global order in every thread.

Check yourself

Quiz

Summary

Summary

  • A thread is an independent path of execution; threads share the heap but have their own stacks.
  • Create threads by extending Thread or implementing Runnable; always call start(), not run().
  • States: NEW → RUNNABLE ↔ BLOCKED / WAITING / TIMED_WAITING → TERMINATED.
  • Unsynchronized shared updates cause race conditions; synchronized methods/blocks give one thread the lock at a time.
  • wait/notifyAll coordinate threads (producer–consumer); consistent lock ordering avoids deadlock.