[Java] 34. 스레드 생성과 실행

2025. 7. 1. 16:02·Java/멀티스레드&동시성

 

스레드 생성과 실행

#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() : 실행 중인 스레드의 이름을 조회한다.

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

참고) 김영한의 실전 자바 - 고급 1편

'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
'Java/멀티스레드&동시성' 카테고리의 다른 글
  • [Java] 37. 메모리 가시성
  • [Java] 36. 스레드 제어와 생명 주기(2)
  • [Java] 35. 스레드 제어와 생명 주기(1)
  • [Java] 33. 프로세스와 스레드
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (462)
      • 개발 일지 (0)
        • Performance (0)
        • TroubleShooting (0)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (0)
      • Software Engineering (36)
        • Test (8)
        • 이론 (18)
        • Clean Code (10)
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (76)
        • Kotlin (0)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 34. 스레드 생성과 실행
상단으로

티스토리툴바