스레드 시작 1
자바 메모리 구조

1. 메서드 영역
: 프로그램을 실행하는데 필요한 공통 데이터 관리 -> 모든 영역에서 공유
> 클래스 정보 : 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드 존재
> static 영역 : static 변수 보관
> 런타임 상수 풀 : 프로그램 실행 시 필요한 공통 리터럴 상수 보관
2. 스택 영역
: 자바 실행 시, 하나의 실행 스택이 생성됨
: 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고 메서드가 종료되면 해당 스택 프레임이 제거됨
* 스레드별 하나의 실행 스택이 생성됨
3. 힙 영역
: 객체와 배열이 생성되는 영역
: 가비지 컬렉션이 이루어지는 주요 영역이며 더 이상 참조되지 않는 객체는 GC에 의해 제거됨
스레드 생성
자바가 예외를 객체로 다루듯, 스레드도 객체로 다룸
HelloThread.java
package thread.start;
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " : run()");
}
}
-> Thread 클래스 상속 & 스레드가 실행할 코드 run() 재정의
HelloThreadMain.java
package thread.start;
public class HelloThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " : main() start");
HelloThread helloThread = new HelloThread();
System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
helloThread.start();
System.out.println(Thread.currentThread().getName() + ": start() 호출 후");
System.out.println(Thread.currentThread().getName() + " : main() end");
}
}
스레드 생성 전
> main() 메서드는 main이라는 이름의 스레드가 실행함
> 자바는 실행 시점에 main이라는 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행
스레드 생성 후
> HelloThread 스레드 객체 생성 후 start() 메서드를 호출하면 자바는 스레드를 위한 별도 스택 공간을 할당
> 스레드 객체 생성 후 start()를 호출해야 스택 공간을 할당 받고 스레드가 작동함
> 스레드에 이름을 주지 않으면 자바는 Thread-0, Thread-1과 같은 임의의 이름 부여
> Thread-0 스레드가 run() 메서드의 스택 프레임을 스택에 올리며 run() 메서드 시작
시간의 흐름으로 분석
1. main 스레드가 HelloThread 인스턴스 생성
2. start() 메서드 호출 시 Thread-0 스레드가 시작되며 Thread-0이 run() 메서드 호출
3. main 스레드가 run()을 실행하는 것이 아닌 Thread-0 스레드가 run()을 실행한다는 것이 핵심!
4. main 스레드는 단지 start()를 통해 Thread-0에게 실행을 지시할 뿐
5. main 스레드와 Thread-0 스레드는 동시에 실행됨
6. main 스레드는 그림의 1~3번을 멈추지 않고 계속 수행하고 run()은 별도 스레드에서 실행
=> 스레드는 동시에 실행되기에 스레드 간 실행 순서는 달라질 수 있음
=> 다양한 실행 결과가 나올 수 있음
=> 스레드는 순서와 실행 기간을 모두 보장하지 않음 = 멀티스레드!
스레드 시작2
start() 대신 run()을 바로 호출할 경우
// helloThread.start();
helloThread.run();
실행 결과
-> 별도 생성한 스레드가 run()을 실행하지 않고 main 스레드가 run()을 호출하고 있음
> 스레드의 start() 메서드는 스레드에 스택 공간을 할당하며 스레드를 시작하는 특별한 메서드
> 해당 스레드에서 run() 메서드를 실행함
데몬 스레드
스레드는 사용자 스레드와 데몬 스레드로 구분할 수 있다.
1. 사용자 스레드(user)
> 프로그램 주요 작업 수행
> 작업이 완료될 때까지 실행
> 모든 user 스레드 종료시 JVM도 종료
2. 데몬 스레드
> 백그라운드에서 보조 작업 수행
> 모든 user 스레드 종료 시 데몬 스레드는 자동으로 종료됨 : JVM은 데몬 스레드 실행 완료를 기다리지 않고 종료됨
데몬스레드 설정 true
package thread.start;
public class DaemonThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
DaemonThread daemonThread = new DaemonThread();
daemonThread.setDaemon(true); // 데몬 스레드 여부 : O
daemonThread.start(); // 실행하자마자 꺼지는 것 확인
System.out.println(Thread.currentThread().getName() + ": main() end");
}
static class DaemonThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
try {
Thread.sleep(10000); // 10초간 실행된다고 가정
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": end()");
}
}
}
-> 실행 결과 : 실행 되자마자 종료됨
-> 메인 스레드는 위 코드에서 main()이 끝나면 종료된다. 메인 스레드가 종료될때 데몬 스레드도 강제로 종료되므로 데몬 스레드의 sleep이 이루어지는 동안 강제 종료된다.
데몬 스레드 설정 false
daemonThread.setDaemon(false); // 데몬 스레드 여부 : X
-> 실행 결과 : 데몬 스레드도 실행되는 것 확인
스레드 생성 - Runnable
스레드를 만들 때는 Thread 클래스를 상속 받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.
HelloRunnable.java
package thread.start;
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
HelloRunnableMain.java
package thread.start;
public class HelloRunnableMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloRunnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
-> 실행결과는 기존과 동일
-> 기존 Thread 상속으로 구현한 것과 스레드와 해당 스레드가 실행할 작업이 서로 분리되어 있다는 점이 다름
Thread 상속 vs Runnable 구현
스레드를 사용할 때는 Runnable 인터페이스를 구현하는 방식을 사용해야 함!!
1. Thread 클래스 상속 방식
(+) 간단한 구현 : Thread 클래스를 상속받아 run() 메서드 재정의
(-) 상속 제한 : 자바는 당일 상속만 허용하기에 이미 다른 클래스를 상속받고 있는 경우 Thread 클래스를 상속받을 수 없음
(-) 유연성 부족 : 인터페이스를 사용하는 방법에 비해 유연성이 떨어짐
2. Runnable 인터페이스 구현 방식
(+) 상속의 자유로움 : 다른 클래스를 상속받아도 문제없이 구현 가능
(+) 코드의 분리 : 스레드와 실행 작업을 분리해 코드 가독성을 높일 수 있음
(+) 여러 스레드가 동일 Runnable 객체를 공유할 수 있어 자원 관리가 효율적
(-) 코드가 약간 복잡해질 수 있음 : Runnable 객체를 생성하고 이를 Thread에 전달하는 과정이 추가됨
로거 만들기
아래와 같이 매번 어떤 스레드가 코드를 실행하는지 확인하는 코드가 너무 길기에 로거를 만들어두자.
System.out.println(Thread.currentThread().getName() + ": run()");
MyLogger.java
package util;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public abstract class MyLogger {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH::mm::ss.SSS");
public static void log(Object obj) {
String time = LocalTime.now().format(formatter);
// %s : printf에서 문자열을 뜻함
// %9s : 문자 출력 시 9칸을 확보한다는 뜻으로 9칸이 차지 않으면 왼쪽에 그 만큼 비워둠
System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
}
}
여러 스레드 만들기
스레드를 100개 생성하고 실행해보자.
package thread.start;
import static util.MyLogger.log;
public class ManyThreadMainV2 {
public static void main(String[] args) {
log("main() start");
HelloRunnable runnable = new HelloRunnable();
for(int i=0; i<100; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
log("main() end");
}
}
-> 실행 결과 : 스레드 실행 순서 보장 X
> 만약 스레드 3개를 생성한다고 가정하면, 스레드 3개는 모두 같은 HelloRunnable 인스턴스를 스레드의 실행 작업으로 전달했다.
> 스레드 3개 모두 HelloRunnable 인스턴스의 run() 메서드를 실행한다.
'인프런 > 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 6. 동기화 - synchronized (0) | 2025.03.13 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 5. 메모리 가시성 (0) | 2025.02.26 |
| [인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 4. 스레드 제어와 생명 주기 2 (0) | 2025.02.14 |
| [인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 3. 스레드 제어와 생명 주기 1 (1) | 2025.02.02 |
| [인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 1. 멀티태스킹과 멀티프로세싱 (1) | 2025.01.06 |