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
- Every Java program is already multi threaded
- Creating a thread
- 1. Implementing Runnable, the preferred way
- 2. Extending Thread, rarely appropriate
- 3. Callable, when a result is needed
- start and run are not the same
- Waiting for a thread
- Sleeping and yielding
- Daemon threads
- Interruption is a request, not a kill
- Uncaught exceptions
- Virtual threads
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Processes and threads
| Process | Thread | |
|---|---|---|
| Memory | Its own address space | Shared with other threads in the process |
| Creation cost | High | Lower |
| Communication | Through the operating system | Through shared memory |
| Isolation | A crash affects only itself | An 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 Runnable | Extend Thread | |
|---|---|---|
| Inheritance | Free to extend something else | Uses up the one superclass |
| Separation | The task is separate from how it runs | Task and mechanism are fused |
| Reusable with executors | Yes | Awkward |
| Lambda friendly | Yes | No |
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();Runnable | Callable<V> | |
|---|---|---|
| Method | run() | call() |
| Returns | nothing | V |
| Checked exceptions | No | Yes |
| Submitted to | Thread or an executor | An 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 threadCalling 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 stopThread.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 ofstart(). - Starting the same thread twice.
- Creating threads directly in application code instead of using an executor.
- Swallowing
InterruptedExceptionwithout restoring the flag. - Relying on
Thread.sleepfor coordination between threads. - Assuming a daemon thread will finish its work before the JVM exits.
Best practices
- Implement
RunnableorCallablerather than extendingThread. - Use an
ExecutorServicerather 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
- Start three threads that each print their name five times, and explain why the output order varies.
- What is the difference in behaviour between
worker.start()andworker.run()? - Write a worker that stops cleanly when interrupted.
- Why is extending
Threadusually the weaker choice? - Explain what happens to a daemon thread when
mainfinishes.
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.