
자바 애플리케이션이 실행되면 JVM은 프로그램의 실행을 위한 메모리 공간을 여러 영역으로 나누어 관리한다. 주요 구성은 다음과 같다.

| 메모리 영역 | 설명 |
|---|---|
| Method Area | 클래스 구조, static 변수, 상수 풀 등 모든 스레드가 공유하는 메타 정보 저장 영역 |
| Heap | new 연산자로 생성된 인스턴스들이 저장되는 공간으로, GC의 관리 대상이 됨 |
| Stack | 스레드마다 독립적으로 생성되며, 메서드 호출 시마다 스택 프레임이 쌓임. 지역 변수 및 연산 흐름이 이곳에 위치함 |
자바에서 스레드를 생성한다는 것은 결국 독립된 Stack 영역을 갖는 실행 흐름(Thread of Execution)을 추가한다는 것이다. 모든 스레드는 Heap 및 Method Area는 공유하지만 Stack은 절대 공유되지 않으며, 이로 인해 각 스레드는 독립적인 지역 변수, 메서드 실행 흐름을 유지할 수 있다.
자바에서 스레드를 생성하는 방법은 크게 두 가지로 나뉜다. 첫 번째는 Thread 클래스를 직접 상속받는 방식이며, 두 번째는 Runnable 인터페이스를 구현하여 Thread 생성자에 전달하는 방식이다.
public class HelloThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
HelloThread thread = new HelloThread();
thread.start();
핵심은 start() 메서드를 호출해야만 운영체제 수준에서 새로운 스레드가 생성된다는 점이다. 만약 단순히 run()을 호출하면 이는 단지 현재(main) 스레드에서 메서드를 실행하는 것과 동일하다.
| 구분 | run() | start() |
|---|---|---|
| 실행 주체 | 현재 스레드(main 등) | JVM이 생성한 새 스레드 |
| 실행 방식 | 일반 메서드 호출 | OS 스레드 생성 후 JVM이 run() 호출 |
| 병렬 실행 여부 | ✖ (동기 실행) | ✔ (비동기 실행) |
일반적으로는 run()은 테스트 용도 외에는 직접 호출하지 않으며, 실제 멀티스레딩을 구현하기 위해서는 반드시 start()를 사용해야 한다.
thread.setDaemon(true); // 반드시 start() 전에 설정해야 함
thread.start();
| 구분 | 설명 |
|---|---|
| 사용자 스레드 | 애플리케이션의 주 작업을 담당하며, 모든 사용자 스레드가 종료되어야 JVM이 종료됨 |
| 데몬 스레드 | 사용자 스레드를 보조하는 역할. 사용자 스레드가 모두 종료되면 자동으로 종료됨 |
데몬 스레드는 주로 GC, 로깅, 백그라운드 작업 등에 활용되며, JVM은 사용자 스레드가 모두 종료되면 데몬 스레드를 강제로 종료시킨다.
보다 유연하고 실용적인 방식은 Runnable 인터페이스를 사용하는 것이다. 이는 실행 로직과 스레드 객체를 분리함으로써 코드의 재사용성과 확장성을 높일 수 있다.
public class HelloRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable);
thread.start();
Runnable을 구현하면 Thread와 실행 로직이 분리되므로, ThreadPoolExecutor나 병렬 처리 프레임워크와 쉽게 통합이 가능하다.
Runnable runnable = new HelloRunnable();
for (int i = 0; i < 3; i++) {
new Thread(runnable).start();
}
하나의 Runnable 인스턴스를 기반으로 여러 스레드를 생성하면, 동일한 실행 로직을 가진 병렬 작업을 손쉽게 구성할 수 있다. 단, 상태를 공유하는 필드가 있을 경우에는 적절한 동기화가 필요하다.
| 방식 | 장점 | 단점 |
|---|---|---|
| Thread 상속 | 코드가 간결함 | 단일 상속 제약, 객체 분리 어려움 |
| Runnable 구현 | 구조적 분리, 재사용 쉬움 | 코드 길이가 다소 길어짐 |
| 익명 클래스 | 지역 스코프에서 빠르게 작성 가능 | 가독성 저하 |
| 람다식 (Java 8+) | 간결하고 선언적 | 람다 문법에 대한 이해 필요 |
자바 8 이후부터는 람다식을 통한 구현이 가장 보편적이며, 코드의 의도를 간결하게 표현할 수 있다.
// 정적 중첩 클래스
static class MyRunnable implements Runnable {
public void run() {
System.out.println("run()");
}
}
new Thread(new MyRunnable()).start();
// 익명 클래스
new Thread(new Runnable() {
public void run() {
System.out.println("run()");
}
}).start();
// 람다식
new Thread(() -> System.out.println("run()")).start();
실행 컨텍스트가 단순하고 일회성 작업인 경우 익명 클래스나 람다식을 활용하는 것이 실용적이다. 다만 로직이 복잡해질 경우에는 클래스 분리를 통해 가독성과 테스트 용이성을 확보하는 것이 좋다.
자바에서 스레드를 만든다는 것은 새로운 실행 흐름을 정의하고, 그 흐름을 위한 독립적인 Stack 공간을 JVM 내부에 구성한다는 의미다. Thread를 직접 상속하거나 Runnable을 구현하는 방식 모두 이를 위한 진입점일 뿐이며, 본질적으로는 운영체제와 JVM이 협력하여 하나의 스레드 컨텍스트를 만드는 과정이라고 볼 수 있다.