관리 메뉴

나구리의 개발공부기록

스레드 생성과 실행, 스레드 시작, 데몬 스레드, 스레드 생성-Runnable, 로거 만들기, 여러 스레드 만들기, Runnable을 만드는 다양한 방법 본문

인프런 - 실전 자바 로드맵/실전 자바 - 고급 1편, 멀티스레드와 동시성

스레드 생성과 실행, 스레드 시작, 데몬 스레드, 스레드 생성-Runnable, 로거 만들기, 여러 스레드 만들기, Runnable을 만드는 다양한 방법

소소한나구리 2025. 2. 9. 23:59
728x90

출처 : 인프런 - 김영한의 실전 자바 - 고급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


더보기

프로젝트 환경 구성

 

1. IDE: 인텔리제이

2. Name: java-adv1

3. Build system: IntelliJ

4. JDK: 21이상(자바 19이상에서 제공하는 기능을 사용함)


1. 스레드 시작

1) 스레드 시작

(1) HelloThread

  • Thread 클래스를 상속하고 스레드가 실행할 코드를 run() 메서드에 재정의
  • Thread.currentThread()를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있음
  • Thread.currentThread().getName(): 실행 중인 스레드의 이름을 조회함
package thread.start;

public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run()");
    }
}

 

(2) HelloThreadMain

  • 앞서 만든 HelloThread 스레드 객체를 생성하고 start() 메서드를 호출
  • start() 메서드는 스레드를 실행하는 아주 특별한 메서드로 helloThread.start()를 호출하면 HelloThread가 run()메서드를 실행함
  • 실행 결과는 스레드의 실행 순서에 따라 다르게 출력될 수 있음

** 주의

  • run() 메서드를 호출하는 것이 아니라 반드시 start() 메서드를 호출해야 스레드에서 run() 코드가 실행됨
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() start
main: start() 호출 전
main: start() 호출 후
Thread-0: run()
main: main() end
*/

 

(3-1) 스레드 생성 전

  • 실행 결과를 보면 main() 메서드는 main이라는 이름의 스레드가 실행하는 것을 확인할 수 있음
  • 프로세스가 최소한 하나는 있어야 코드를 실행할 수 있으므로 자바는 실행 시점에 main이라는 이름의 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행함

(3-2) 스레드 생성 후

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

(3-3) 메서드를 실행하면 스택 위에 스택 프레임이 쌓임

  • main 스레드는 main() 메서드의 스택 프레임을 스택에 올리면서 시작하고 직접 만드는 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 시작함

(4) 시간의 흐름으로 분석

  • main 스레드가 HelloThread 인스턴스를 생성하고 이때 스레드에 이름을 부여하지 않으면 자바가 Thread-0, Thread-1과 같은 임의의 이름을 부여함
  • start() 메서드를 호출하면 Thread-0 스레드가 시작되면서 Thread-0 스레드가 run()메서드를 호출함
  • 여기서 핵심은 main 스레드가 run()메서드를 실행하는 것이 아니라 Thread-0 스레드가 run() 메서드를 실행한다는 점임
  • main 스레드는 단지 start() 메서드를 통해 Thread-0 스레드에게 실행을 지시할 뿐이며 main 스레드는 다른 스레드에게 일을 시작하라고 지시만 하고 바로 start() 메서드를 빠져나옴
  • 이제 main스레드와 Thread-0 스레드는 동시에 실행됨
  • main 스레드의 입장에서 보면 그림의 1, 2, 3번 코드를 멈추지 않고 계속 수행하며 run() 메서드는 main이 아닌 별도의 스레드에서 실행됨

(5) 스레드 간 실행 순서는 보장하지 않음

  • 스레드는 동시에 실행되기 때문에 스레드 간에 실행 순서는 얼마든지 달라질 수 있으므로 다양한 실행 결과가 나올 수 있음
  • CPU 코어가 2개여서 물리적으로 정말 동시에 실행될 수도 있고 하나의 CPU 코어에 시간을 나누어 실행 될 수도 있음
  • 그리고 한 스레드가 얼마나 오랜기간 실행되는지도 보장하지 않으므로 한 스레드가 먼저 다 수행된 다음에 다른 스레드가 수행될 수도 있고 둘이 완전히 번갈아 가면서 수행되는 경우도 있음
  • 즉, 스레드는 순서와 실행 기간을 모두 보장하지 않으며 이것이 바로 멀티 스레드임

2) start() vs run()

(1) BadThreadMain

  • 스레드의 start() 대신에 재정의한 run() 메서드를 직접 호출하면 HelloThread가 아니라 main이 run()메서드를 호출하는 것을 알 수 있음
  • 자바를 처음 실행하면 main 스레드가 main() 메서드를 호출하면서 시작하고 main 스레드는 HelloThread 인스턴스에 있는 run() 이라는 메서드를 호출함
  • main 스레드가 run() 메서드를 실행했기 때문에 main 스레드가 사용하는 스택위에 run() 스택 프레임이 올라가며 결과적으로 main스레드에서 모든 것을 처리한 것이됨
  • 스레드의 start() 메서드는 스레드에 스택 공간을 할당하면서 스레드를 시작하는 아주 특별한 메서드임
  • main 스레드가 아닌 별도의 스레드에서 재정의한 run() 메서드를 실행하려면 반드시 start() 메서드를 호출해야 함
package thread.start;

public class BadThreadMain {
    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.run();  // run() 직접 실행
        System.out.println(Thread.currentThread().getName() + ": start() 호출 후");

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }
}
/* 실행 결과
main: main() start
main: start() 호출 전
main: run()
main: start() 호출 후
main: main() end
*/

2. 데몬 스레드

1) 데몬 스레드

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

  • 프로그램의 주요 작업을 수행함
  • 작업이 완료될 때까지 실행되며 모든 user 스레드가 종료되면 JVM도 종료됨

(2) 데몬 스레드

  • 백그라운드에서 보조적인 작업을 수행함
  • 모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료됨
  • JVM은 데몬 스레드의 실행 완료를 기다리지 않고 종료되며 데몬 스레드가 아닌 모든 스레드가 종료되면 자바 프로그램도 종료됨

** 용어 - 데몬

  • 그리스 신화에서 데몬은 신과 인간 사이의 중간적 존재로 보이지 않게 활동하며 일상적인 일들을 도왔음
  • 이런 의미로 컴퓨터 과학에서는 사용자에게 직접적으로 보이지 않으면서 시스템의 백그라운드에서 작업을 수행하는 것을 데몬 스레드, 데몬 프로세스라고 함
  • 예를 들어 사용하지 않는 파일이나 메모리를 정리하는 작업들이 있음

(3) DaemonThreadMain

  • static 내부 클래스로 Thread를 상속 받아서 쓰레드를 생성하고 run() 메서드를 재정의할 때 10초간 기다렸다가 다음 메서드가 실행되도록 작성
  • setDaemon(true): 데몬 스레드로 설정, 데몬 스레드 여부는 start() 실행 전에 결정해야하며 이후에는 변경되지 않으며 해당 메서드를 호출하지않으면 기본적으로 user 스레드로 동작함
  • 실행 결과를 보면 Thread-0이 데몬 스레드로 설정되고 출력되자 마자 유일한 user스레드인 main 스레드가 종료되면서 자바 프로그램이 종료되어 run() end가 출력되지 않는 것을 확인할 수 있음

** 참고

  • run() 메서드 안에 Thread.sleep()을 호출할 때 체크 예외인 InterruptedException을 밖으로 던질 수 없고 반드시 잡아야함
  • run() 메서드는 체크 예외를 밖을 던질 수 없는데 이부분은 뒤에서 설명함
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);
        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) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + ": run() end");
        }
    }
}
/* 실행 결과
main: main() start
main: main() end
Thread-0: run() start
*/

 

(2) setDaemon(false)

  • 만약 setDaemon(false)로 설정하거나 해당 메서드를 주석처리하고 호출해보면 main 스레드가 종료되어도 user 스레드인 Thread-0종료될 때까지 자바 프로그램이 종료되지 않는 것을 확인할 수 있음
  • Thread-0: run() end가 출력되고 user스레드인 main스레드와 Thread-0스레드가 모두 종료되어야 자바 프로그램이 종료됨

3. 스레드 생성-Runnable

1) Runnable

(1) HelloRunnable

  • 스레드를 만들 때는 Thread 클래스를 상속 받는 방법과 Runnable 인터페이스를 구현하는 방법이 있는데 실무에서는 주로 Runnable을 구현하는 방법을 사용함
  • Runnable인터페이스를 구현하면 무조건 run() 메서드를 재정의해야함
package thread.start;

public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": run()");
    }
}

 

(2) HelloRunnableMain

  • start()로 스레드를 실행하는 것과 실행 결과는 기존의 Thread로 동작할 때와 동일함
  • 차이가 있다면 스레드와 해당 스레드가 실행할 작업이 서로 분리되어있다는 점으로 스레드 객체를 생성할 때 실행할 작업을 생성자로 전달하면 됨
package thread.start;

public class HelloRunnableMain {
    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName() + ": main() start");

        HelloRunnable helloRunnable = new HelloRunnable();
        Thread thread = new Thread(helloRunnable);
        thread.start();

        System.out.println(Thread.currentThread().getName() + ": main() end");
    }
}
/* 실행 결과
main: main() start
main: main() end
Thread-0: run()
*/

 

2) Thread 상속 vs Runnable 구현

(1) Thread 클래스 상속받는 방식

  • 장점
    • 간단한 구현: Thread 클래스를 상속 받아 run() 메서드만 재정의하면 됨
  • 단점
    • 상속의 제한: 자바는 단일 상속만을 허용하므로 이미 다른 클래스를 상속받고 있는 경우 Thread 클래스를 상속 받을 수 없음
    • 유연성 부족: 인터페이스를 사용하는 방법에 비해 유연성이 떨어짐

(2) Runnable 인터페이스를 구현하는 방식

  • 장점
    • 상속의 자유로움: Runnable 인터페이스 방식은 다른 클래스를 상속받아도 문제없이 구현할 수 있음
    • 코드의 분리: 스레드와 실행할 작업을 분리하여 코드의 가독성을 높일 수 있음
    • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있음
  • 단점
    • Runnable 객체를 생성하고 이를 Thread에 전달하는 과정이 추가되어 코드가 약간 복잡해짐

(3) 정리

  • 스레드를 사용할 때는 Thread를 상속 받는 방법보다 Runnable 인터페이스를 구현하는 방식을 사용해야 함
  • 스레드와 실행할 작업을 명확히 분리하고 인터페이스를 사용하므로 Thread 클래스를 직접 상속하는 방식보다 더 유연하고 유지보수 하기 쉬운 코드를 만들 수 있음

4. 로거 만들기

1) 로거 만들기

(1) MyLogger

  • 지금은 어떤 스레드가 코드를 실행하는지 출력해보기 위해서 상당히 긴 출력문을 매번 작성해야하는데, 현재 시간, 스레드 이름, 출력 내용등을 편리하게 한번에 확인할 수 있도록 하는 로거를 작성
  • 프로젝트 전반에 사용되는 유틸리티이므로 util이라는 새로운 패키지에 작성
  • DateTimeFormatter: 현재 시간을 원하는 포맷으로 출력하기 위함
  • printf:
    • %s는 문자열을 뜻하며 인자를 순서대로 사용함
    • 여기서는 현재 시간, 스레드 이름, 출력할 객체를 순서대로 사용하였음
    • 출력할 객체의 타입을 Object으로 한 이유는 %s를 사용하면 toString()을 사용해서 문자열로 변환 후 출력해주기 때문에 문자열 뿐만 아니라 객체도 출력할 수 있음
    • %9s는 문자를 출력할 때 9칸을 확보한다는 뜻으로 9칸이 전부 채워지지 않으면 왼쪽에 그만큼 비워둠(오른쪽 정렬 효과)
package util;

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);
        System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
    }
}

 

(2) MyLoggerMain

  • MyLogger.log()를 사용해보면 스레드 이름과 출력시간, 출력할 객체가 깔끔하게 출력되는 것을 확인할 수 있음
  • 스태틱임포트를 사용하면 메서드 이름만으로 간단히 사용할 수 있음
  • 스레드를 학습할 때는 스레드 이름, 그리고 해당 스레드가 언제 실행되었는지 확인하는 것이 중요하므로 스레드강의에서는System.out.println() 대신 MyLogger를 사용하여 진행
  • 실무에서는 로그 라이브러리를 사용함
package util;

import static util.MyLogger.*;

public class MyLoggerMain {
    public static void main(String[] args) {
        log("hello thread");
        log(123);
    }
}
/* 출력 결과
21:16:54.806 [     main] hello thread
21:16:54.807 [     main] 123
*/

5. 여러 스레드 만들기

(1) ManyThreadMainV1

  • 스레드 3개를 생성할 때 모두 같은 HelloRunnable 인스턴스를 스레드의 실행 작업으로 전달했으므로 Thread-0, Thread-1,Thread-2는 모두 HelloRunnable 인스턴스에 있는 run() 메서드를 실행함
  • 스레드의 실행 순서는 보장되지 않으므로 실행결과는 실행할 때마다 달라짐
package thread.start;

public class ManyThreadMainV1 {
    public static void main(String[] args) {
        log("main() start");

        HelloRunnable helloRunnable = new HelloRunnable();
        Thread thread1 = new Thread(helloRunnable);
        thread1.start();
        Thread thread2 = new Thread(helloRunnable);
        thread2.start();
        Thread thread3 = new Thread(helloRunnable);
        thread3.start();

        log("main() end");
    }
}
/* 실행 결과
21:21:42.139 [     main] main() start
21:21:42.142 [     main] main() end
Thread-0: run()
Thread-2: run()
Thread-1: run()
*/

 

(2) ManyThreadMainV2

  • 반복문을 활용하여 스레드를 유동적으로 여러개 생성할 수 있음
  • 실행해보면 마찬가지로 스레드의 실행순서가 보장되지 않는 것을 확인할 수 있음
package thread.start;

public class ManyThreadMainV2 {
    public static void main(String[] args) {
        log("main() start");

        HelloRunnable helloRunnable = new HelloRunnable();

        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(helloRunnable);
            thread.start();
        }
  
        log("main() end");
    }
}

6. Runnable을 만드는 다양한 방법

1) Runnable을 만드는 다양한 방법

(1) InnerRunnableMainV1 - 정적 중첩 클래스 사용

  • 특정 클래스 안에서만 사용되는 경우 중첩 클래스를 사용하면 됨
package thread.start;

public class InnerRunnableMainV1 {
    public static void main(String[] args) {
        log("main() start");

        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();

        log("main() end");
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            log("run()");
        }
    }
}

 

(2) 익명 클래스 사용

  • 특정 메서드 안에서만 간단히 정의하고 사용하고 싶다면 익명 클래스를 사용하면 됨
package thread.start;

public class InnerRunnableMainV2 {
    public static void main(String[] args) {
        log("main() start");

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log("run()");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

        log("main() end");
    }

}

 

(3) 익명 클래스 변수 없이 직접 전달

  • 익명 클래스를 참조하는 변수를 만들지 않고 직접 전달할 수 있음
package thread.start;

public class InnerRunnableMainV3 {
    public static void main(String[] args) {
        log("main() start");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                log("run()");
            }
        });
        thread.start();

        log("main() end");
    }


}

 

(4) 람다

  • 람다를 사용하면 메서드(함수) 코드 조각을 전달할 수 있음
  • 아직 람다를 학습하지 않았기 때문에 강의에서는 정적 중첩 클래스나 익명 클래스를 주로 사용할 예정
package thread.start;

public class InnerRunnableMainV4 {
    public static void main(String[] args) {
        log("main() start");

        Thread thread = new Thread(() -> log("run()"));
        thread.start();

        log("main() end");
    }
}

7. 문제와 풀이

1) Thread 상속

(1) 요구사항

  • 다음 요구 사항에 맞게 멀티 스레드 프로그램을 작성
  • 1. Thread 클래스를 상속받은 CounterThread라는 스레드 클래스를 생성
  • 2. 이 스레드는 1부터 5까지의 숫자를 1초 간격으로 출력해야하며 출력은 log() 기능을 사용
  • 3. 실행 결과를 참고하여 main() 메서드에서 CounterThread 스레드 클래스를 만들고 실행
더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest1Main {
    // 여기에 코드 작성
}

 

실행 결과

09:46:23.329 [ Thread-0] value: 1

09:46:24.332 [ Thread-0] value: 2

09:46:25.338 [ Thread-0] value: 3

09:46:26.343 [ Thread-0] value: 4

09:46:27.349 [ Thread-0] value: 5

 

(2) 정답

더보기
package thread.start.test;

public class StartTest1Main {

    public static void main(String[] args) {
        new CounterThread().start();
    }

    static class CounterThread extends Thread {
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                log("value: " + i);
                sleep(1000);
            }
        }

        private void sleep(int millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

2) Runnable 구현

(1) 요구사항

  • 다음 요구 사항에 맞게 멀티 스레드 프로그램을 작성
  • 1. CounterRunnable 이라는 이름의 클래스를 만들고 Runnable 인터페이스를 구현
  • 2. CounterRunnable는 1부터 5까지의 숫자를 1초 간격으로 출력해야하며 출력은 log() 기능을 사용
  • 3. 실행 결과를 참고하여 main() 메서드에서 CounterRunnable의 인스턴스를 이용하여 Thread를 생성하고 실행
  • 4. 스레드의 이름은 counter로 지정해야 함
더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest2Main {
    // 여기에 코드 작성
}

 

실행 결과

09:53:36.705 [ counter] value: 1

09:53:37.713 [ counter] value: 2

09:53:38.719 [ counter] value: 3

09:53:39.725 [ counter] value: 4

09:53:40.726 [ counter] value: 5

 

(2) 정답

더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest2Main {
    public static void main(String[] args) {
        CounterRunnable counterRunnable = new CounterRunnable();
        Thread counter = new Thread(counterRunnable, "counter");
        counter.start();
    }

    static class CounterRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                log("value: " + i);
                sleep(1000);
            }
        }

        private void sleep(int millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

3) Runnable 익명 클래스 구현

(1) 요구사항

  • 2번의 문제2를 익명 클래스로 구현

 

(2) 정답

더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest3Main {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    log("value: " + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }, "counter").start();
    }
}

4) 여러 스레드 사용

(1) 요구사항

  • Thread-A, Thread-B 두 스레드를 만들고 Thread-A는 1초에 한 번씩 "A"를 출력하고 Thread-B는 0.5초에 한 번씩 "B"를 출력함
  • 이 프로그램은 강제 종료할 때까지 계속 실행되어야 함
더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest4Main {
    // 여기에 코드 작성
}

 

실행 결과

10:04:27.000 [ Thread-A] A

10:04:27.000 [ Thread-B] B

10:04:27.507 [ Thread-B] B

10:04:28.006 [ Thread-A] A

10:04:28.012 [ Thread-B] B

10:04:28.518 [ Thread-B] B

10:04:29.011 [ Thread-A] A

10:04:29.023 [ Thread-B] B

... 무한 실행

 

(2) 정답

더보기
package thread.start.test;

import static util.MyLogger.log;

public class StartTest4Main {

    public static void main(String[] args) {
        new Thread(new PrintRunnable("A", 1000), "Thread-A").start();
        new Thread(new PrintRunnable("B", 500), "Thread-B").start();
    }

    static class PrintRunnable implements Runnable {

        private String content;
        private int ms;

        public PrintRunnable(String content, int ms) {
            this.content = content;
            this.ms = ms;
        }

        @Override
        public void run() {
            while (true) {
                log(content);
                try {
                    Thread.sleep(ms);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

 

728x90