Java
multithreading
thread.run()
thread.start()
concurrency

When would you call java's thread.run instead of thread.start?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Java, thread.start() creates a new thread of execution, while calling thread.run() directly executes synchronously on the current thread. Understanding this difference is essential for concurrency correctness. Most real concurrent work should use start, and direct run calls are usually for testing or deliberate synchronous execution.

What start Actually Does

start asks the JVM to schedule a new thread and then invokes run on that new thread.

java
1public class Demo {
2    public static void main(String[] args) {
3        Thread t = new Thread(() -> {
4            System.out.println("worker thread: " + Thread.currentThread().getName());
5        });
6
7        t.start();
8        System.out.println("main thread: " + Thread.currentThread().getName());
9    }
10}

Output order can vary because two threads run concurrently.

What Direct run Call Does

Calling run directly is just a normal method call on the current thread.

java
1public class DemoRun {
2    public static void main(String[] args) {
3        Thread t = new Thread(() -> {
4            System.out.println("inside run: " + Thread.currentThread().getName());
5        });
6
7        t.run();
8        System.out.println("after run: " + Thread.currentThread().getName());
9    }
10}

Both lines execute on main thread in this case.

Legitimate Use Cases for run Directly

Direct run calls can be valid when you intentionally want synchronous behavior:

  • Unit tests that reuse task logic without spawning threads.
  • Fallback execution paths when threading is disabled.
  • Educational demos of lifecycle differences.

Still, make intent explicit in naming and comments to avoid confusion.

Better Alternatives in Modern Java

For production concurrency, prefer executors instead of manual thread lifecycle handling.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public class ExecutorDemo {
5    public static void main(String[] args) {
6        ExecutorService pool = Executors.newFixedThreadPool(2);
7        pool.submit(() -> System.out.println(Thread.currentThread().getName()));
8        pool.shutdown();
9    }
10}

Executors improve resource control and reduce thread-management errors.

Testing Without Accidental Concurrency

If you want deterministic tests, execute task logic directly through callable methods rather than thread objects.

java
1class Task {
2    String compute() {
3        return "ok";
4    }
5}
6
7public class TaskTestLike {
8    public static void main(String[] args) {
9        Task task = new Task();
10        System.out.println(task.compute());
11    }
12}

This avoids timing flakiness while preserving logic coverage.

Common Interview and Debugging Scenario

A classic debugging issue occurs when developers expect parallel work after calling run directly. Printing thread names quickly reveals the mistake.

java
1Runnable work = () -> System.out.println("running on " + Thread.currentThread().getName());
2
3Thread t = new Thread(work, "worker-1");
4t.run();   // synchronous
5// t.start(); // asynchronous

Using this small snippet in onboarding docs helps newer developers avoid the most common threading misunderstanding.

Lifecycle Rules and Exceptions

A Thread instance can be started only once. Calling start twice raises IllegalThreadStateException.

java
Thread t2 = new Thread(() -> {});
t2.start();
// t2.start(); // would throw IllegalThreadStateException

Direct run calls do not enforce this lifecycle rule, which is another reason they should not be used as a substitute for concurrency.

Better Abstractions for Most Code

In modern Java applications, use CompletableFuture or executors for asynchronous composition and error handling.

java
1import java.util.concurrent.CompletableFuture;
2
3CompletableFuture<Void> f = CompletableFuture.runAsync(() -> {
4    System.out.println("async task");
5});
6f.join();

These abstractions provide clearer control than manual thread creation in most business applications.

Practical Rule of Thumb

If your intent is concurrency, call start or use executor abstractions. If your intent is a plain method call in current thread, call a regular method directly rather than invoking run on a thread object.

Common Pitfalls

  • Calling run expecting parallel execution.
  • Starting the same thread instance multiple times and getting errors.
  • Mixing manual thread creation with executor usage inconsistently.
  • Using direct run in production paths unintentionally.
  • Writing tests that depend on thread scheduling order.

Summary

  • start creates concurrent execution on a new thread.
  • Direct run executes synchronously on current thread.
  • Use direct run only when synchronous behavior is intentional.
  • Prefer executor frameworks for production concurrency.
  • Keep concurrency intent explicit to avoid subtle bugs.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.