🧵 Concurrency Fundamentals · Advanced

Creating threads in Java

Thread + Runnable, Thread.ofPlatform, start() vs run().

🧩 The mysteryOne method makes your code run in parallel. Its near-twin runs it on the SAME thread, with no error and no warning. Can you tell start() from run()?

Task vs worker

A Runnable is the task: what to do. A Thread is the worker: who does it. You hand the task to the worker, then tell the worker to go.

Runnable task = () -> System.out.println("hi");
Thread t = new Thread(task);
t.start();   // a new thread runs task

What start() really does

start() asks the JVM to create a new thread, and that new thread then calls run(). start() returns immediately, so the caller keeps going in parallel. Calling run() yourself is just an ordinary method call on the current thread: no new thread at all.

🔮 Predict it

Your turn

What does this print?

Thread t = new Thread(() ->
    System.out.println(
        Thread.currentThread().getName()));
t.setName("worker");
t.start();
t.join();
  1. worker
  2. main
  3. Thread-0
Show the answer

worker. start() launches the new thread, and the lambda runs on it, so currentThread() is the thread we named "worker". join() makes main wait until it has printed.

start() vs run()

✗ Looks parallel
Thread t = new Thread(() -> download());
t.run();
showUi();

run() executes the download on the calling thread. The caller blocks until it's done, and currentThread() inside is still main.

✓ Really parallel
Thread t = new Thread(() -> download());
t.start();
showUi();

start() creates a new thread for the download; the caller moves on to showUi() immediately.

⚠️ The trap

Threads are single-use

A Thread object can be started only once. Even after it has finished, a second start() throws IllegalThreadStateException. Need to run the task again? Create a new Thread, or use a thread pool.

Thread t = new Thread(() -> {});
t.start();
t.join();
t.start(); // IllegalThreadStateException

The modern builder

Since Java 21, Thread.ofPlatform() returns a builder. Name the thread, then start(task) creates and starts it in one call. If you want to start it later, use unstarted(task) instead.

Thread w = Thread.ofPlatform()
    .name("io-1")
    .start(() -> fetch());
 
Thread later = Thread.ofPlatform()
    .unstarted(() -> fetch());
later.start();
💼 In the real world

On the job

UI apps have one thread that draws the screen. Run a slow download on it and the app freezes. The run()-instead-of-start() slip does exactly that: the "background" job silently runs on the caller. Android is so strict about this that network calls on its main thread throw NetworkOnMainThreadException.

Key takeaways

  1. Runnable = the task; Thread = the worker
  2. start() launches a new thread; run() is a plain method call
  3. A Thread object can be started only once
  4. Thread.ofPlatform().name("w").start(task) builds and starts
🤯 Did you know?

Thread itself implements Runnable. That's why a run() method exists on every Thread at all, and why calling it by mistake compiles without a peep.

Practice questions

What does this print?

Thread t = new Thread(() ->
    System.out.println(
        Thread.currentThread().getName()));
t.setName("worker");
t.run();
  1. main
  2. worker
  3. Thread-0
  4. Throws IllegalThreadStateException
Check your answer

main. run() is an ordinary method call, so the lambda executes on the calling thread — main. Only start() would run it on the new thread named "worker".

What does this print?

Thread t = new Thread(() -> {});
t.start();
t.join();
t.start();
  1. Throws IllegalThreadStateException
  2. Runs the task a second time
  3. Nothing; the second start() is ignored
  4. Compile error
Check your answer

Throws IllegalThreadStateException. A Thread object is single-use. Once it has been started, calling start() again throws IllegalThreadStateException — create a new Thread (or use a pool) instead.

Next: a thread's life story in six states, from NEW to TERMINATED, and the one state that screams "lock contention" in a thread dump.