Skip to main content

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

ProcessThread
DefinitionAn entire running programA unit of execution within a process
MemoryIndependent memory spaceShares process memory
Creation costHighLow
CommunicationIPC (complex)Shared memory (simpler, but requires synchronization)
ExampleChrome, Word, a gameA 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.

note

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");
}
}

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");
}
}
warning

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
StateDescription
NEWThread object created, start() not yet called
RUNNABLEAfter start() — waiting for CPU or actively running
BLOCKEDWaiting to acquire a synchronized lock
WAITINGWaiting indefinitely via wait() or join()
TIMED_WAITINGTime-limited wait via sleep(n), join(n), wait(n)
TERMINATEDrun() 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());
}
}
tip

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");
}
}
warning

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
}
}
MethodNew ThreadExecuted ByAsynchronous?
run()NoCurrent thread (e.g., main)No (sequential)
start()YesNewly created threadYes (parallel)
Pro Tip

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.