Skills101

Java Programming · Lesson 14 of 15 · Updated ·

Multithreading in Java — Threads, Runnable, synchronized and Executors

A thread is a path of execution inside a program. Multithreading lets one program do several things at once — download a file while the screen stays responsive, or process thousands of requests on a server.

Stack vs heap: every thread gets its own stack for local variables, but all threads share the same heap — which is why shared objects need care

Key idea: each thread has its own stack, but all threads share the heap. Sharing objects between threads is where bugs come from.

1. Process vs thread

ProcessThread
Whata running programa path of work inside a process
Memoryits ownshares the process's heap
Cost to createhighlow
Communicationslow (IPC)fast (shared objects)

2. Creating threads

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            System.out.println(Thread.currentThread().getName() + " sum=" + sum);
        };
        Thread t1 = new Thread(task, "worker-1");
        t1.start();          // start() runs task in a NEW thread
        t1.join();           // wait for it to finish
        System.out.println("main done");
    }
}
worker-1 sum=5050
main done
  • Prefer implementing Runnable (or a lambda) over extending Thread — your class can still extend something else.
  • Call start(), not run(). run() just runs the code in the current thread.
  • join() waits for a thread to finish.

3. Thread life cycle

New → start() → Runnable → (waiting for a lock / sleep / wait) Blocked / Waiting / Timed waiting → back to Runnable → Terminated.

4. Race conditions and synchronized

When two threads update the same variable, updates can be lost. count++ is really three steps (read, add, write).

public class Main {
    static int unsafe = 0;
    static int safe = 0;
    static synchronized void incSafe() { safe++; }

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                unsafe++;        // not protected
                incSafe();       // one thread at a time
            }
        };
        Thread a = new Thread(job), b = new Thread(job);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println("safe = " + safe);
        System.out.println("unsafe is " + (unsafe == 200_000 ? "lucky this time" : "less than 200000 (lost updates!)"));
    }
}

safe is always 200000. unsafe is usually less — the exact number changes every run, which is exactly the problem.

  • synchronized lets only one thread at a time into the method (it locks the object/class).
  • For simple counters, AtomicInteger is faster: counter.incrementAndGet().

5. ExecutorService: the modern way

Creating threads by hand is rare in real code. Use a thread pool:

import java.util.*;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        List<Future<Integer>> results = new ArrayList<>();
        for (int n = 1; n <= 5; n++) {
            final int x = n;
            results.add(pool.submit(() -> x * x));     // a Callable returns a value
        }
        int total = 0;
        for (Future<Integer> f : results) total += f.get();   // waits for each result
        pool.shutdown();
        System.out.println("sum of squares = " + total);
    }
}
sum of squares = 55

6. Deadlock

Thread A holds lock 1 and waits for lock 2; thread B holds lock 2 and waits for lock 1. Both wait forever. Prevent it by always taking locks in the same order, holding them briefly, or using timeouts (tryLock).

Interview quick answers

  • start() vs run()? start() creates a new thread; run() is a normal method call.
  • sleep() vs wait()? sleep pauses without releasing locks; wait releases the lock and waits for notify().
  • volatile? makes a variable's latest value visible to all threads (no atomicity).
  • Thread-safe collections? ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue.

Common mistakes

  • Calling run() instead of start().
  • Sharing mutable data without synchronization.
  • Forgetting shutdown() on an ExecutorService (the program may not exit).

Quick recap

  • Threads share the heap, have their own stacks.
  • Runnable/lambda + start(); join() to wait.
  • Protect shared data with synchronized or atomic classes.
  • Use ExecutorService thread pools; avoid deadlock with consistent lock order.

Practice what you learned

📝 Quiz: Java Multithreading5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →