Java

[JAVA] 스레드 생성과 실행 방법

쌈뽕코딩 2025. 6. 18. 20:30

자바 메모리 구조

Java에서 스레드를 이해하려면, 먼저 자바의 메모리 구조를 간단히 짚고 넘어갈 필요가 있습니다.

자바 프로그램이 실행되면 JVM은 여러 영역으로 메모리를 나누어 각기 다른 역할을 수행합니다.

이 구조는 스레드가 어떤 메모리를 공유하고, 어떤 메모리를 개별적으로 갖는지를 이해하는 데 중요한 기반이 됩니다.

이제 자바의 기본적인 메모리 구성부터 살펴보겠습니다.

JVM 메모리 구조

메서드 영역(Method Area): JVM이 프로그램에 필요한 클래스 정보를 모아두는 공간으로, 모든 스레드가 공유합니다.

  • 클래스 정보: 클래스 이름, 상속 관계, 메서드 및 필드 정보 등
  • 바이트코드: 메서드 및 생성자의 실행 코드
  • static 변수: 모든 인스턴스가 공유하는 static 필드
  • 런타임 상수 풀: 문자열 리터럴 및 컴파일 시 생성된 상수들

스택 영역 (Stack Area): 자바 프로그램이 실행될 때, 각 스레드마다 실행용 스택이 생성됩니다.

  • 스택 프레임은 메서드가 호출될 때마다 만들어지는 작은 기억 공간입니다.
  • 각 스택 프레임은 지역 변수, 중간 연산 결과, 그리고 메서드 호출 정보가 담깁니다.

  • 메서드가 끝나면 해당 스택 프레임은 사라집니다.
  • 스택 영역은 각 스레드별로 하나의 실행 스택이 생성된다. 따라서 스레드 수 만큼 스택이 생성 된다.

힙 영역 (Heap Area): 힙 영역은 프로그램에서 생성한 객체(인스턴스)배열이 저장되는 공간입니다.

  • 여러 스레드가 공유하는 메모리 영역이며, JVM의 가비지 컬렉터(GC)가 동작하는 주요 대상입니다.
  • 더 이상 참조되지 않는 객체들은 GC에 의해 자동으로 삭제됩니다.

🤔 스택 프레임

스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다.
메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.

 


스레드 생성

스레드를 직접 만들어서 특정 작업을 실행해보겠습니다. 자바에서 스레드를 만드는 방법은 크게 두 가지가 있는데,
1)Thread 클래스를 상속하는 방법2)Runnable 인터페이스를 구현하는 방법입니다.

먼저, Thread 클래스를 상속받아 스레드를 생성하는 방법부터 살펴보겠습니다.

 

1) 스레드 생성 - Thread 상속

자바에서는 대부분의 요소를 객체로 취급합니다.
예외도 객체로 처리하듯, 스레드 역시 객체로 다뤄집니다.
스레드가 필요할 때는 스레드 객체를 만들어서 실행하면 됩니다.
public class HelloThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run");
    }
}
  • Thread 클래스를 상속하고, 스레드가 실행할 코드를 run() 메서드에 재정의한다.
  • Thread.currentThread() 를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.
  • Thread.currentThread().getName(): 실행 중인 스레드의 이름을 조회한다.
public class HelloThreadMain {

    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ": main() start");

        HelloThread helloThread = new HelloThread();
        System.out.println(helloThread.getName() + ": start() 호출 전");
        helloThread.start();
        System.out.println(helloThread.getName() + ": start() 호출 후");

        System.out.println(thread.getName() + ": main() end");
    }
}
  • 스레드의 start() 메서드는 스레드에 스택 공간을 할당하면서 스레드를 시작하는 아주 특별한 메서드이다.

  • start() 를 호출하면 해당 스레드(HelloThread)run() 메서드를 실행한다.
  • run() 메서드가 아니라 반드시 start() 메서드를 호출해야 한다. 그래야 별도의 스레드에서 run() 코드가 실행된다.
  • 참고로 스레드 간 순서를 보장하기 않기 때문에 실행 결과는 스레드의 실행 순서에 따라 약간 다를 수 있다.

스레드 생성 전

실행 결과를 보면 main() 메서드는 main 이라는 이름의 스레드가 실행하는 것을 확인할 수 있다.

프로세스가 작동하려면 스레드가 최소한 하나는 있어야 한다. 그래야 코드를 실행할 수 있다.

자바는 실행 시점에 main 이라는 이름의 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행한다.

 

스레드 생성 후

  • HelloThread 스레드 객체를 생성한 다음에 start() 메서드를 호출하면 자바는 스레드를 위한 별도의 스택 공간을 할당한다.
  • 스레드 객체를 생성하고, 반드시 start() 를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.
  • 스레드에 이름을 주지 않으면 자바는 스레드에 Thread-0 , Thread-1 과 같은 임의의 이름을 부여한다.
  • 새로운 Thread-0 스레드가 사용할 전용 스택 공간이 마련되었다.
  • Thread-0 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 run() 메서드를 시작한다.

start() 메서드를 호출하면 새로운 스레드인 Thread-0이 시작되며, 이 스레드가 run() 메서드를 실행합니다.

중요한 점은 run() 메서드를 main 스레드가 실행하는 것이 아니라, 새로 생성된 Thread-0 스레드가 실행한다는 것입니다.

즉, main 스레드는 단지 start()를 호출해 작업을 시작하라고 지시할 뿐이고, 실제 작업은 별도의 스레드가 수행합니다.

start() 호출 이후, main 스레드는 해당 스레드가 끝나기를 기다리지 않고 바로 다음 코드로 넘어갑니다.

스레드 간 실행 순서는 보장하지 않습니다. ❗️ ❗️ ❗️ 
스레드는 동시에 실행되기 때문에 스레드 간에 실행 순서는 얼마든지 달라질 수 있습니다.
따라서 다양한 실행 결과가 나올 수 있는 것입니다.

 

스레드 간의 실행 순서는 얼마든지 달라질 수 있습니다.
CPU
코어가 2개여서 물리적으로 정말 동시에 실행될 수도 있고, 하나의 CPU 코어에 시간을 나누어 실행될 수도 있습니다.

그리고 한 스레드가 얼마나 오랜기간 실행되는지도 보장하지 않습니다.

한 스레드가 먼저 다 수행된 다음에 다른 스레드가 수행될 수도 있고, 둘이 완전히 번갈아 가면서 수행되는 경우도 있습니다.

"스레드는 순서와 실행 기간을 모두 보장하지 않는다." 이것이 바로 멀티 스레드의 특징입니다.

 

🤔 스레드의 start() 대신에 재정의한 run() 메서드를 직접 호출하면 어떻게 될까?

helloThread.run(); //run 직접 호출

결론은 Thread-0 스레드가 run() 을 실행하는 것이 아니라, main 스레드가 run() 메서드를 호출하게 됩니다.
자바를 처음 실행하면 main 스레드가 main() 메서드를 호출하면서 시작합니다.

main 스레드는 HelloThread 인스턴스에 있는 run() 이라는 메서드를 호출한다.

main 스레드가 run() 메서드를 실행했기 때문에 main 스레드가 사용하는 스택 위에 run() 스택 프레임이 올라가게 됩니다.

즉, main 스레드에서 모든 것을 처리한 것이 됩니다.

 

2) 스레드 생성 - Runnable

public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run()");
    }
}
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");
    }
}
  • 실행 결과는 기존과 같다. 다른 점은 스레드와 해당 스레드가 실행할 작업이 서로 분리되어 있다는 점이다.

  • 스레드 객체를 생성할 때, 실행할 작업을 생성자로 전달하면 된다.

3) Thread 상속 vs Runnable 구현

 

스레드를 만들 때는 Thread 클래스를 직접 상속하는 것보다 Runnable 인터페이스를 구현하는 방식을 사용하는 것이 권장됩니다.

두 방식이 서로 장단점이 있지만, Runnable 방식 상속 제한이 없고 코드의 유연성이 높아 더 권장됩니다.

 

Thread 클래스 상속 방식

✅ 장점

  1. 간단한 구현: Thread 클래스를 상속받아 run() 메서드만 재정의하면 된다.

❌ 단점

  1. 상속의 제한: 자바는 단일 상속만을 허용하므로 이미 다른 클래스를 상속받고 있는 경우 Thread 클래스를 상속 받을 수 없다.
  2. 유연성 부족: 인터페이스를 사용하는 방법에 비해 유연성이 떨어진다.

 Runnable 인터페이스를 구현 방식

✅ 장점

  1. 상속의 자유로움: Runnable 인터페이스 방식은 다른 클래스를 상속받아도 문제없이 구현할 수 있다.
  2. 코드의 분리: 스레드와 실행할 작업을 분리하여 코드의 가독성을 높일 수 있다.
  3. 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다.

❌ 단점

  1. 코드가 약간 복잡해질 수 있다.
  2. Runnable 객체를 생성하고 이를 Thread 에 전달하는 과정이 추가된다.

정리하자면 Runnable 인터페이스를 구현하는 방식을 사용합시다.

스레드와 실행할 작업을 명확히 분리하고, 인터페이스를 사용하므로 Thread 클래스를 직접 상속하는 방식보다

더 유연하고 유지보수 하기 쉬운 코드를 만들 수 있습니다. 


데몬 스레드

스레드는 사용자(user) 스레드 데몬(daemon) 스레드 2가지 종류로 구분할 수 있습니다.

놀랍게도 데몬이 그 데몬이 맞습니다.

그리스 신화에서 데몬은 신과 인간 사이의 중간적 존재로, 보이지 않게 활동하며 일상적인 일들을 도왔다.
이런 의미로 컴퓨터 과학에서는 사용자에게 직접적으로 보이지 않으면서 시스템의 백그라운드에서 작업을 수행하는 것을
데몬 스레드, 데몬 프로세스라 한다.

 

사용자 스레드 (non-daemon 스레드)

  • 프로그램의 주요 작업을 수행한다.
  • 작업이 완료될 때까지 실행된다.
  • 모든 user 스레드가 종료되면 JVM도 종료된다.
  • 예시: 파일 다운로드, 사용자 입력을 받는 UI 이벤트 처리, 비즈니스 로직

데몬 스레드

  • 백그라운드에서 보조적인 작업을 수행한다.
  • 모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.
  • 예시: 가비지 컬렉터, 자동 저장 기능, 세션 만료 체크

JVM은 데몬 스레드의 실행 완료를 기다리지 않고 종료됩니다.

데몬 스레드가 아닌 모든 스레드가 종료되면, 자바 프로그램도 종료됩니다.

public class DaemonThreadMain {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true); // 데몬 스레드 여부
        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);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + ": run() end");
        }
    }
}
  • setDaemon(true) : 데몬 스레드로 설정한다.
  • 데몬 스레드 여부는 start() 실행 전에 결정해야 한다. 이후에는 변경되지 않는다.
  • 기본 값은 false 이다. (user 스레드가 기본)
  • 유일한 user 스레드인 main 스레드가 종료되면서 자바 프로그램도 종료된다.
  • 따라서 run() end 가 출력되기 전에 프로그램이 종료된다.