Threads in Java: Processes, Runnable and Thread

A thread is an independent path of execution inside one process, and Java gives you several ways to start one.

Processes and threads

ProcessThread
MemoryIts own address spaceShared with other threads in the process
Creation costHighLower
CommunicationThrough the operating systemThrough shared memory
IsolationA crash affects only itselfAn uncaught error can end the process

Threads share memory, which is what makes them fast to coordinate and also what makes concurrency difficult. Every problem in the rest of this section comes from that shared memory.

Every Java program is already multi threaded

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());   // main
}

The JVM also runs threads for garbage collection, finalisation and signal handling.

Creating a thread

1. Implementing Runnable, the preferred way

Runnable task = () -> {
    for (int i = 1; i <= 3; i++) {
        System.out.println(Thread.currentThread().getName() + " step " + i);
    }
};

Thread worker = new Thread(task, "worker-1");
worker.start();

2. Extending Thread, rarely appropriate

class Reporter extends Thread {
    @Override
    public void run() {
        System.out.println("Reporting from " + getName());
    }
}

new Reporter().start();
Implement RunnableExtend Thread
InheritanceFree to extend something elseUses up the one superclass
SeparationThe task is separate from how it runsTask and mechanism are fused
Reusable with executorsYesAwkward
Lambda friendlyYesNo
Prefer Runnable. It separates what to do from how it runs, which is exactly what lets the same task be submitted to a thread pool later.

3. Callable, when a result is needed

Callable<Integer> job = () -> {
    Thread.sleep(100);
    return 42;                 // may return a value and may throw a checked exception
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(job);
System.out.println(future.get());
executor.shutdown();
RunnableCallable<V>
Methodrun()call()
ReturnsnothingV
Checked exceptionsNoYes
Submitted toThread or an executorAn executor only

start and run are not the same

Thread worker = new Thread(task);

worker.start();   // creates a new thread and calls run() there
worker.run();     // just an ordinary method call on the current thread

Calling run() directly is a classic mistake: the code executes, everything appears to work, and no concurrency happens at all. A thread also cannot be started twice; a second start() throws IllegalThreadStateException.

Waiting for a thread

Thread worker = new Thread(task);
worker.start();

worker.join();                       // wait indefinitely
worker.join(Duration.ofSeconds(5));  // wait with a timeout, Java 19 onwards

System.out.println("worker finished");

Sleeping and yielding

try {
    Thread.sleep(500);                    // milliseconds
    Thread.sleep(Duration.ofMillis(500)); // Java 19 onwards
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // restore the flag
    return;
}

sleep does not release any lock the thread holds. That surprises people and is a frequent cause of stalls.

Daemon threads

Thread background = new Thread(this::pollForUpdates);
background.setDaemon(true);    // must be set before start()
background.start();

The JVM exits when the last non daemon thread finishes. A daemon thread is stopped abruptly at that point, so it must never hold data that has to be flushed.

Interruption is a request, not a kill

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            doWork();
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();   // restore and exit the loop
            break;
        }
    }
    cleanUp();
});

worker.start();
worker.interrupt();     // asks the thread to stop

Thread.stop() was removed because it could leave shared data half updated. Interruption is cooperative: it sets a flag and causes blocking calls to throw, and the task decides how to finish tidily.

Uncaught exceptions

Thread worker = new Thread(() -> {
    throw new IllegalStateException("something failed");
});

worker.setUncaughtExceptionHandler(
        (thread, error) -> logger.severe(thread.getName() + " failed: " + error));

worker.start();

An exception in a thread ends only that thread. Without a handler it prints a trace and is otherwise silent, which is why failures in background threads often go unnoticed.

Virtual threads

Thread virtual = Thread.ofVirtual().start(() -> System.out.println("light"));
virtual.join();

Thread platform = Thread.ofPlatform().name("worker-1").start(task);

Java 21 introduced virtual threads: threads scheduled by the JVM rather than by the operating system. They are cheap enough to create millions of, which makes a thread per request practical for I/O bound work. Platform threads remain the right choice for CPU bound work.

Common mistakes

  • Calling run() instead of start().
  • Starting the same thread twice.
  • Creating threads directly in application code instead of using an executor.
  • Swallowing InterruptedException without restoring the flag.
  • Relying on Thread.sleep for coordination between threads.
  • Assuming a daemon thread will finish its work before the JVM exits.

Best practices

  • Implement Runnable or Callable rather than extending Thread.
  • Use an ExecutorService rather than managing threads by hand.
  • Give threads meaningful names; they appear in every stack trace.
  • Always restore the interrupt flag when catching InterruptedException.
  • Set an uncaught exception handler so background failures are visible.
  • Consider virtual threads for I/O bound workloads on Java 21 and later.

Practice

  1. Start three threads that each print their name five times, and explain why the output order varies.
  2. What is the difference in behaviour between worker.start() and worker.run()?
  3. Write a worker that stops cleanly when interrupted.
  4. Why is extending Thread usually the weaker choice?
  5. Explain what happens to a daemon thread when main finishes.

Conclusion

A thread is an independent path through shared memory. Express work as a Runnable or Callable, let an executor run it, use start rather than run, and treat interruption as the cooperative shutdown mechanism it is.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.