스레드 생성과 실행
#Java/멀티스레드
정리
자바 메모리 구조 복습
자바 메모리 구조
스택 영역은 더 정확히는 각 스레드별로 하나의 실행 스택이 생성된다. 따라서 스레드 수 만큼 스택이 생성된다. 지금은 스레드를 1개만 사용하므로 스택도 하나이다. 이후 스레드를 추가할 것인데, 그러면 스택도 스레드 수 만큼 증가한다.
스레드 생성
자바에서 스레드는 어떻게 생성할까?
스레드를 만들 때는 Thread 클래스를 상속 받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.
스레드 생성 - Thread 상속
자바가 예외를 객체로 다루듯이, 스레드도 객체로 다룬다. 스레드가 필요하면, 스레드 객체를 생성해서 사용하면 된다.
package thread.start;
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
Thread클래스를 상속하고, 스레드가 실행할 코드를run()메서드에 재정의한다.Thread.currentThread()를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.- ex)
Thread.currentThread().getName(): 실행 중인 스레드의 이름을 조회한다.
- ex)
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(); // 절대로 .run()을 실행하지 말자.
System.out.println(Thread.currentThread().getName() + ": start() 호출 후");
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
- 앞서 만든
HelloThread스레드 객체를 생성하고start()메서드를 호출한다. start()메서드는 스레드를 실행하는 아주 특별한 메서드이다.start()를 호출하면HelloThread스레드가run()메서드를 실행한다.
주의!run() 메서드가 아니라 반드시 start() 메서드를 호출해야 한다. 그래야 별도의 스레드에서 run()이 실행된다.
실행 결과
main: main() start
main: start() 호출 전
main: start() 호출 후
Thread-0: run()
main: main() end
- 참고로 실행 결과는 스레드의 실행 순서에 따라 약간 다를 수 있다.
스레드 생성 전
실행 결과를 보면 main() 메서드는 main 이라는 이름의 스레드가 실행하는 것을 확인할 수 있다.
프로세스가 작동하려면 스레드가 최소한 하나는 있어야 한다. 그래야 코드를 실행할 수 있다.
자바는 실행 시점에 main 이라는 이름의 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행한다.
스레드 생성 후
HelloThread스레드 객체를 생성한 다음에start()메서드를 호출하면 자바는 스레드를 위한 별도의 스택 공간을 할당한다.- 스레드 객체를 생성하고, 반드시
start()를 호출해야 스택 공간을 할당 받고 스레드가 작동한다. - 스레드에 이름을 주지 않으면 자바는 스레드에
Thread-0,Thread-1과 같은 임의의 이름을 부여한다. - 새로운
Thread-0스레드가 사용할 전용 스택 공간이 마련되었다. Thread-0스레드는run()메서드의 스택 프레임을 스택에 올리면서run()메서드를 시작한다.- 메서드를 실행하면 스택 위에 스택 프레임이 쌓인다
- 여기서 핵심은
main스레드가run()메서드를 실행하는 게 아니라Thread-0스레드가run()메서드를 실행한다는 점이다.main스레드는 단지start()메서드를 통해Thread-0스레드에게 실행을 지시할 뿐이다. 지시만 하고,start()메서드를 바로 빠져나온다.
- 이제
main스레드와Thread-0스레드는 동시에 실행된다.
스레드 간 실행 순서, 실행 기간을 모두 보장하지 않는다.
스레드는 동시에 실행되기 때문에 스레드 간에 실행 순서는 얼마든지 달라질 수 있다. 위 코드 또한 어떤 스레드가 빨리 실행되었는지에 따라 실행 결과가 달라진다.
CPU 코어가 2개여서 물리적으로 정말 동시에 실행될 수도 있고, 하나의 CPU 코어에 시간을 나누어 실행될 수도 있다. 그리고 한 스레드가 얼마나 오랜 기간 실행되는지도 보장하지 않는다. 한 스레드가 먼저 다 수행된 다음에 다른 스레드가 수행될 수도 있고, 둘이 번갈아 가면서 수행되는 경우도 있다.
만약 메인스레드에서 start()가 아닌 run()을 호출한다면?
run() 직접 호출
- 실행 결과를 보면 별도의 스레드가
run()을 실행하는 것이 아니라,main스레드가run()메서드를 호출하여,main스레드의 스택 위에run()스택 프레임이 올라간다. Thread-0는 아무일도 하지 않는다.HelloThread helloThread = new HelloThread();는 그냥 힙영역에 있는 객체 덩어리일 뿐이다.start()를 호출해줘야 스택 영역의 스택 프레임도 만들게 되고, OS에다가 스레드를 만들겠다는 시스템 콜을 날리게 된다. 그러면 스레드가 운영체제에 할당이 되면서 매칭이 된다.
결론: main 스레드가 아닌 별도의 스레드에서 재정의한 run() 메서드를 실행하려면, 반드시 start() 메서드를 호출해야 한다.
데몬 스레드
스레드는 사용자(user) 스레드와 데몬(daemon) 스레드 2가지 종류로 구분할 수 있다.
사용자 스레드 (non-daemon 스레드)
- 프로그램의 주요 작업을 수행한다.
- 작업이 완료될 때까지 실행된다.
- 모든 user 스레드가 종료되면 JVM도 종료된다.
위에서 다뤘던 예시에서도, main스레드가 종료된다고 해서 JVM이 종료되는 건 아니다. Thread-0와 main 스레드 모두 종료되어야 JVM이 종료된다.
데몬 스레드
- 백그라운드에서 보조적인 작업을 수행한다.
- 모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.
- CS에서는, 사용자에게 직접적으로 보이지 않으면서 시스템의 백그라운드에서 작업을 수행하는 것을 데몬 스레드, 데몬 프로세스라 한다
데몬 스레드 예시
public class DaemonThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
DaemonThread daemonThread = new DaemonThread();
daemonThread.setDaemon(true); // 데몬 스레드 여부
// 스레드 상속받으면 디폴트가 userThread라, 데몬 스레드 여부를 지정해줘야 함.
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() start");
try {
Thread.sleep(10000); // 10초간 실행
} catch (InterruptedException e) {
// `run()` 메서드 안에서 `Thread.sleep()` 을 호출할 때 체크 예외인 `InterruptedException` 을 밖으로 던질 수 없고 반드시 잡아야 한다.
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": run() end");
}
}
}
setDaemon(true): 데몬 스레드로 설정한다.- 데몬 스레드 여부는
start()실행 전에 결정해야 하며, 이후에는 변경되지 않는다. - 기본 값은
false(즉, user 스레드가 기본이다).
setDaemon(true)인 경우 실행결과
main: main() start
main: main() end
Thread-0: run() start
Thread-0는 데몬 스레드로 설정된다.- 유일한 user 스레드인
main스레드가 종료되면서 자바 프로그램도 종료된다. - 따라서
run() end가 출력되기 전에 프로그램이 종료된다. setDaemon(false)로 설정하면 User 스레드가 되어, user 스레드인main스레드와Thread-0스레드가 모두 종료되면 자바 프로그램도 종료된다.
Runnable
이번에는 Runnable 인터페이스를 구현하는 방식으로 스레드를 생성해보자. (실무에서는 이 방법을 사용한다)
(Runnable : 뭔가 실행될 수 있는 작업이라는 뜻)
Runnable 인터페이스
package java.lang;
public interface Runnable {
void run();
}
- 자바가 제공하는 스레드 실행용 인터페이스
package thread.start;
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
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의 분리
thread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
실행 결과는 기존과 같다. 차이가 있다면, 스레드와 해당 스레드가 실행할 작업이 서로 분리되어 있다는 점이다.
스레드 객체를 생성할 때, 실행할 작업을 생성자로 전달하면 된다.
그러면 Thread 상속과 Runnable 구현 중 어떤 방식을 사용해야 할까?
Thread 클래스 상속 방식의 경우, 이미 다른 클래스를 상속받고 있는 경우 Thread 클래스를 상속 받을 수 없다. 또한, 인터페이스에 비해 유연성이 떨어진다. 반면에 Runnable 인터페이스를 구현하는 방식의 경우 상속이 자유롭고, 스레드와 실행 작업을 분리할 수 있다. 또한 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리에 효율적이다.
Runnable 인터페이스를 구현하는 방식을 사용하자.
여러 스레드 만들기
스레드 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");
}
}
실행 결과는 매 번 다를 수 있다. 스레드의 실행 순서는 보장되지 않는다.
Runnable을 만드는 다양한 방법
특정 클래스 안에서만 사용되는 경우 이렇게 중첩 클래스 or 익명 클래스를 사용하면 된다.
public class InnerRunnableMainV1 {
public static void main(String[] args) {
log("main() start");
Runnable runnable = new MyRunnable();
/* 익명 클래스
Runnable runnable = new Runnable() {
@Override
public void run() {
log("run()");
}
};
*/
Thread thread = new Thread(runnable);
/* 익명 클래스: 변수 없이 전달
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log("run()");
}
});
*/
/* 람다
Thread thread = new Thread(() -> log("run()"));
*/
thread.start();
log("main() end");
}
static class MyRunnable implements Runnable {
@Override
public void run() {
log("run()");
}
}
}
'Java > 멀티스레드&동시성' 카테고리의 다른 글
| [Java] 38. 동기화 - synchronized (0) | 2025.07.01 |
|---|---|
| [Java] 37. 메모리 가시성 (0) | 2025.07.01 |
| [Java] 36. 스레드 제어와 생명 주기(2) (0) | 2025.07.01 |
| [Java] 35. 스레드 제어와 생명 주기(1) (0) | 2025.07.01 |
| [Java] 33. 프로세스와 스레드 (0) | 2025.07.01 |