Ch 13.1 Multi-Threading Basics (Thread & Runnable)
Multi-threading in Java is a core technology that allows multiple tasks to run simultaneously. A single process can contain multiple threads of execution running in parallel.
1. Process vs Thread
| Process | Thread | |
|---|---|---|
| Definition | An entire running program | A unit of execution within a process |
| Memory | Independent memory space | Shares process memory |
| Creation cost | High | Low |
| Communication | IPC (complex) | Shared memory (simpler, but requires synchronization) |
| Example | Chrome, Word, a game | A tab in Chrome rendering/networking/script in parallel |
Multi-process: Each process has independent memory — one crashing doesn't affect others. Multi-thread: Memory is shared (efficient), but synchronization is required.
When a Java program starts, the JVM creates one process, and the main thread runs within it.
2. Advantages and Disadvantages of Multi-Threading
Advantages
- Improved CPU utilization: while one thread waits, others use the CPU
- Resource sharing: threads within the same process share memory efficiently
- Better responsiveness: separating UI and worker threads keeps screens from freezing
- Higher throughput: parallel execution reduces overall processing time
Disadvantages
- Synchronization problems: concurrent access to shared resources can corrupt data
- Deadlock: threads waiting indefinitely for each other's resources
- Difficult debugging: non-deterministic execution order makes bugs hard to reproduce
- Context-switching overhead: too many threads can degrade performance
3. Creating Threads
Extending the Thread Class
class MyThread extends Thread {
private String taskName;
public MyThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("[" + getName() + "] " + taskName + " - " + i);
try {
Thread.sleep(500); // pause 0.5 seconds
} catch (InterruptedException e) {
System.out.println(taskName + " interrupted");
return;
}
}
System.out.println("[" + getName() + "] " + taskName + " done!");
}
}
public class ThreadInheritExample {
public static void main(String[] args) {
MyThread t1 = new MyThread("Download");
MyThread t2 = new MyThread("Convert");
t1.setName("Download-Thread");
t2.setName("Convert-Thread");
t1.start(); // runs run() in a new thread
t2.start(); // runs run() in another new thread
System.out.println("Main thread: both tasks started");
}
}
Implementing Runnable (Recommended)
Since Java does not support multiple inheritance, implementing Runnable is the preferred approach in practice — especially when the class needs to extend another class.
class MyRunnable implements Runnable {
private String taskName;
public MyRunnable(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] "
+ taskName + " - " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt status
return;
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
Runnable r1 = new MyRunnable("Task A");
Runnable r2 = new MyRunnable("Task B");
Thread t1 = new Thread(r1, "Thread-1");
Thread t2 = new Thread(r2, "Thread-2");
t1.start();
t2.start();
}
}
Lambda Thread (Java 8+)
public class LambdaThreadExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("Lambda thread running: " + i);
}
});
t1.start();
System.out.println("Main thread continues");
}
}
Calling run() directly does not create a new thread — it executes like a normal method on the current thread. Always call start() to have the JVM create a new thread and execute run() in it.
4. Thread Life Cycle
NEW → RUNNABLE → (RUNNING) → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
| State | Description |
|---|---|
| NEW | Thread object created, start() not yet called |
| RUNNABLE | After start() — waiting for CPU or actively running |
| BLOCKED | Waiting to acquire a synchronized lock |
| WAITING | Waiting indefinitely via wait() or join() |
| TIMED_WAITING | Time-limited wait via sleep(n), join(n), wait(n) |
| TERMINATED | run() method has finished (normally or via exception) |
public class ThreadStateExample {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
});
System.out.println("After creation: " + t.getState()); // NEW
t.start();
System.out.println("After start: " + t.getState()); // RUNNABLE
Thread.sleep(500);
System.out.println("During sleep: " + t.getState()); // TIMED_WAITING
t.join();
System.out.println("After finish: " + t.getState()); // TERMINATED
}
}
5. Key Thread Methods
sleep() — Pause
public class SleepExample {
public static void main(String[] args) {
System.out.println("Start");
try {
Thread.sleep(2000); // pause current thread for 2 seconds
} catch (InterruptedException e) {
System.out.println("Interrupted!");
}
System.out.println("Resume after 2 seconds");
}
}
join() — Wait for Another Thread
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker thread: starting heavy computation...");
try { Thread.sleep(3000); } catch (InterruptedException e) {}
System.out.println("Worker thread: computation complete!");
});
worker.start();
System.out.println("Main: waiting for worker...");
worker.join(); // main thread waits until worker finishes
System.out.println("Main: worker done, starting post-processing");
}
}
interrupt() — Send Interrupt Signal
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
System.out.println("Thread: starting long task (10s)");
Thread.sleep(10000);
System.out.println("Thread: done"); // not reached if interrupted
} catch (InterruptedException e) {
System.out.println("Thread: interrupted, exiting");
}
});
t.start();
Thread.sleep(2000); // wait 2 seconds
t.interrupt(); // send interrupt signal to thread
System.out.println("Main: interrupt signal sent");
}
}
Other Key Methods
public class ThreadMethodExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {}, "my-thread");
// Name
System.out.println("Name: " + t.getName()); // my-thread
t.setName("new-name");
// Priority (1–10, default 5)
System.out.println("Priority: " + t.getPriority()); // 5
t.setPriority(Thread.MAX_PRIORITY); // set to 10
// Alive check
t.start();
System.out.println("Is alive: " + t.isAlive()); // true
// Current thread reference
Thread current = Thread.currentThread();
System.out.println("Current thread: " + current.getName());
}
}
Thread priority is only a hint — the OS scheduler makes the actual decision. Priority alone cannot guarantee execution order.
6. Daemon Threads
Daemon threads are background helper threads (e.g., Garbage Collector, auto-save, monitoring). When all non-daemon threads finish, daemon threads are automatically terminated.
public class DaemonThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread monitor = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println("[Monitor] System check... Memory: "
+ Runtime.getRuntime().freeMemory() / 1024 + "KB");
} catch (InterruptedException e) {
return;
}
}
});
monitor.setDaemon(true); // must be set BEFORE start()!
monitor.start();
System.out.println("Main work started");
for (int i = 1; i <= 3; i++) {
Thread.sleep(1500);
System.out.println("Main work " + i + "/3 complete");
}
System.out.println("Main work done — daemon thread will be auto-terminated");
}
}
setDaemon(true) must be called beforestart(). You cannot make an already-running thread a daemon.
7. Thread Groups
Group related threads together for bulk management:
public class ThreadGroupExample {
public static void main(String[] args) throws InterruptedException {
ThreadGroup ioGroup = new ThreadGroup("IO-Group");
ThreadGroup calcGroup = new ThreadGroup("Calc-Group");
Thread t1 = new Thread(ioGroup, () -> {
try { Thread.sleep(3000); } catch (InterruptedException e) {}
}, "IO-Thread-1");
Thread t2 = new Thread(ioGroup, () -> {
try { Thread.sleep(3000); } catch (InterruptedException e) {}
}, "IO-Thread-2");
Thread t3 = new Thread(calcGroup, () -> {
try { Thread.sleep(3000); } catch (InterruptedException e) {}
}, "Calc-Thread-1");
t1.start(); t2.start(); t3.start();
System.out.println("IO group active count: " + ioGroup.activeCount()); // 2
System.out.println("Calc group active count: " + calcGroup.activeCount()); // 1
ioGroup.interrupt(); // interrupt all threads in the IO group
System.out.println("IO group interrupt sent");
}
}
8. Practical Example: Multi-Thread Download Simulation
import java.util.ArrayList;
import java.util.List;
class FileDownloader implements Runnable {
private final String fileName;
private final int fileSizeMB;
private boolean completed = false;
public FileDownloader(String fileName, int fileSizeMB) {
this.fileName = fileName;
this.fileSizeMB = fileSizeMB;
}
@Override
public void run() {
System.out.printf("[%s] Download started (%dMB)%n", fileName, fileSizeMB);
try {
Thread.sleep(fileSizeMB * 100L); // assume 100ms per MB
} catch (InterruptedException e) {
System.out.println("[" + fileName + "] Download interrupted!");
return;
}
completed = true;
System.out.printf("[%s] Download complete!%n", fileName);
}
public boolean isCompleted() { return completed; }
public String getFileName() { return fileName; }
}
public class MultiThreadDownload {
public static void main(String[] args) throws InterruptedException {
String[][] files = {
{"spring-boot.jar", "50"},
{"mysql-connector.jar", "30"},
{"jackson-databind.jar", "20"},
{"slf4j-api.jar", "10"},
{"hibernate-core.jar", "80"}
};
// Sequential download
System.out.println("=== Sequential Download ===");
long startSeq = System.currentTimeMillis();
int totalMB = 0;
for (String[] f : files) {
int mb = Integer.parseInt(f[1]);
totalMB += mb;
System.out.printf("[%s] Downloading (%dMB)...%n", f[0], mb);
Thread.sleep(mb * 100L);
System.out.printf("[%s] Done%n", f[0]);
}
long seqTime = System.currentTimeMillis() - startSeq;
System.out.printf("Sequential done: %dMB total, elapsed %dms%n%n", totalMB, seqTime);
// Parallel download
System.out.println("=== Parallel Download ===");
long startPar = System.currentTimeMillis();
List<Thread> threads = new ArrayList<>();
List<FileDownloader> dls = new ArrayList<>();
for (String[] f : files) {
FileDownloader dl = new FileDownloader(f[0], Integer.parseInt(f[1]));
Thread t = new Thread(dl);
dls.add(dl);
threads.add(t);
t.start();
}
for (Thread t : threads) t.join();
long parTime = System.currentTimeMillis() - startPar;
System.out.printf("Parallel done: elapsed %dms (%.1fx faster than sequential)%n",
parTime, (double) seqTime / parTime);
}
}
Sample output:
=== Sequential Download ===
[spring-boot.jar] Downloading (50MB)...
[spring-boot.jar] Done
...
Sequential done: 190MB total, elapsed 19000ms
=== Parallel Download ===
[spring-boot.jar] Download started (50MB)
[mysql-connector.jar] Download started (30MB)
...
Parallel done: elapsed 8000ms (2.4x faster than sequential)
9. start() vs run() — The Key Difference
public class StartVsRunExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("Running thread: " + Thread.currentThread().getName());
});
// Calling run() directly — executes on the MAIN thread (no new thread!)
t.run(); // Output: Running thread: main
// Calling start() — JVM creates a new thread, runs run() in it
t = new Thread(() -> {
System.out.println("Running thread: " + Thread.currentThread().getName());
});
t.start(); // Output: Running thread: Thread-1
}
}
| Method | New Thread | Executed By | Asynchronous? |
|---|---|---|---|
run() | No | Current thread (e.g., main) | No (sequential) |
start() | Yes | Newly created thread | Yes (parallel) |
In production code, avoid creating raw Thread objects directly. Prefer ExecutorService (thread pools) to control the number of threads, reuse threads, and improve system stability.