- Learn to create threads by extending Thread class
- Understand how to implement the Runnable interface
- Use lambda expressions for concise thread creation
- Master the difference between start() and run()
Creating and Starting Threads in Java
You know what threads are. Now let's create them. Java gives you two main approaches: extend Thread or implement Runnable. Each has its place. Modern Java adds more options with lambdas and Callable.
Method 1: Extending the Thread Class
Most straightforward: extend Thread and override run():
public class MyThread extends Thread {
@Override
public void run() {
// This code runs in the new thread
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + ": Count " + i);
try {
Thread.sleep(500); // Pause for 500ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("Counter-1");
thread2.setName("Counter-2");
thread1.start(); // Starts thread1
thread2.start(); // Starts thread2
System.out.println("Main thread continues...");
}
}
Important: Call start(), not run()!
start()creates a new thread and callsrun()on that threadrun()directly executes the code on the current thread (no new thread created)
thread.start(); // Correct: New thread executes run()
thread.run(); // Wrong: Current thread executes run() - no multithreading!
Method 2: Implementing Runnable
A more flexible approach is implementing the Runnable interface:
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + ": Count " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Runnable task1 = new MyRunnable("Task-1");
Runnable task2 = new MyRunnable("Task-2");
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
Why Prefer Runnable?
| Extending Thread | Implementing Runnable |
|---|---|
| Uses your single inheritance slot | Allows extending another class |
| Thread and task logic coupled | Separates task from execution |
| Creates Thread subclass | More flexible, reusable |
Best practice: Prefer Runnable - it separates "what to do" from "how to run it."
Method 3: Lambda Expressions (Modern Java)
Since Runnable is a functional interface (one abstract method), you can use lambdas:
public class LambdaThreads {
public static void main(String[] args) {
// Full lambda syntax
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Thread 1: " + i);
}
});
// Compact for single statement
Thread thread2 = new Thread(() -> System.out.println("Hello from Thread 2!"));
// With method reference
Thread thread3 = new Thread(LambdaThreads::doWork);
thread1.start();
thread2.start();
thread3.start();
}
private static void doWork() {
System.out.println("Doing work in " + Thread.currentThread().getName());
}
}
Thread Constructor Options
The Thread class provides several constructors:
// Just a Runnable
new Thread(runnable);
// Runnable with name
new Thread(runnable, "MyThread");
// Just a name (requires extending Thread)
new Thread("MyThread");
// With ThreadGroup
new Thread(threadGroup, runnable, "MyThread");
// With stack size (rarely used)
new Thread(threadGroup, runnable, "MyThread", stackSize);
Passing Data to Threads
Via Constructor
public class DownloadTask implements Runnable {
private final String url;
private final String destination;
public DownloadTask(String url, String destination) {
this.url = url;
this.destination = destination;
}
@Override
public void run() {
System.out.println("Downloading from " + url + " to " + destination);
// Actual download logic
}
}
// Usage
Thread download = new Thread(new DownloadTask("http://example.com/file.zip", "/downloads"));
download.start();
Via Captured Variables (Lambdas)
String filename = "data.txt";
int retries = 3;
Thread thread = new Thread(() -> {
// Can access filename and retries (must be effectively final)
System.out.println("Processing " + filename + " with " + retries + " retries");
});
thread.start();
Waiting for Threads: join()
The join() method waits for a thread to complete:
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker: Starting long task...");
try {
Thread.sleep(3000); // Simulate 3-second task
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Worker: Task complete!");
});
worker.start();
System.out.println("Main: Waiting for worker...");
worker.join(); // Block until worker finishes
System.out.println("Main: Worker finished, continuing...");
}
}
join() with Timeout
worker.join(5000); // Wait at most 5 seconds
if (worker.isAlive()) {
System.out.println("Worker still running after 5 seconds");
} else {
System.out.println("Worker completed");
}
Combining Multiple Threads
public class MultipleThreadsExample {
public static void main(String[] args) throws InterruptedException {
List<Thread> workers = new ArrayList<>();
// Create and start 5 worker threads
for (int i = 1; i <= 5; i++) {
int taskId = i; // Effectively final for lambda
Thread worker = new Thread(() -> {
System.out.println("Task " + taskId + " starting");
try {
Thread.sleep((long) (Math.random() * 2000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " complete");
}, "Worker-" + i);
workers.add(worker);
worker.start();
}
// Wait for all workers to complete
for (Thread worker : workers) {
worker.join();
}
System.out.println("All tasks completed!");
}
}
Getting Results from Threads
Runnable doesn't return a value. For that, use Callable<V>:
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
// Callable returns a value
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42; // The result
};
// Need ExecutorService to run Callable
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
System.out.println("Task submitted, doing other work...");
// Get result (blocks until available)
Integer result = future.get();
System.out.println("Result: " + result);
executor.shutdown();
}
}
Common Mistakes
1. Calling run() Instead of start()
// WRONG - runs on main thread, not a new thread
thread.run();
// CORRECT - creates new thread
thread.start();
2. Starting a Thread Twice
Thread thread = new Thread(task);
thread.start();
thread.start(); // IllegalThreadStateException!
3. Ignoring InterruptedException
// BAD - swallows the interrupt
catch (InterruptedException e) {
// Do nothing
}
// GOOD - restore interrupt status or handle properly
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore flag
return; // Exit gracefully
}
Summary
Creating threads: extend Thread (simple cases), implement Runnable (preferred—separates task from execution), use lambdas (concise), or Callable when you need results. Always call start(), never run() directly. Use join() to wait for completion. Handle InterruptedException properly by restoring the interrupt status. A thread can only be started once.
