Programming-[Backend]/Java

자바 기초 강의 정리 - 3. 멀티태스킹(쓰레드) - 1: Thread, Runnable, Group, Daemon

컴퓨터 탐험가 찰리 2024. 6. 8. 17:43
728x90
반응형

인프런 얄코의 제대로 파는 자바 강의를 듣고 정리한 내용이다.

 

중요하거나 실무를 하면서 놓치고 있었던 부분들 위주로만 요약 정리한다.

자세한 내용은 강의를 직접 수강하는 것이 좋다.

https://www.inflearn.com/course/%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%8C%8C%EB%8A%94-%EC%9E%90%EB%B0%94/dashboard

 

 


 

1. 쓰레드

 

프로세스 vs 쓰레드

프로세스는 컴퓨터에서 실행 중인 프로그램이라고 보면 된다. 기본적으로 프로세스간 자원이 공유되지는 않는다. 쓰레드는 한 프로세스 안에서 일어나는 여러 실행 흐름을 의미한다. 이 쓰레드는 같은 프로세스 내의 공유 자원을 사용할 수 있기 때문에 공유 자원 사용에 유의해야한다.

 

쓰레드 생성 방법

다음 두 가지의 방법이 있다.

  • Thread 클래스 상속
  • Runnable 구현

 

Runnable 인터페이스를 구현하는 방법이 인터페이스의 유연함 때문에 보다 많이 사용된다. 그리고 실제로 만들어보면  Thread를 상속받는 방식은 run 메서드를 override를 강제하지 않지만, Runnable은 인터페이스라 run 메서드를 override하도록 IDE에서 알려준다. 그리고 아래 코드에서처럼 생성 방식이 약간 다르다.

//정의
public class Thread1 extends Thread {
        @Override
        public void run() {
            
        }
    }
    
public class MyRunnable implements Runnable {

    @Override
    public void run() {

    }
}
    
    
//생성 방식
Thread thread1 = new Thread1();
Thread thread2 = new Thread(new MyRunnable());
// new MyRunnable 대신 람다식으로 바로 run 메서드를 구현해도 됨

 

run 메서드에는 쓰레드 생성 후 쓰레드가 처리할 작업을 정의한다.

 

 

쓰레드 동시 실행

main 함수에서 이렇게 생성한 쓰레드들을 만들고, run 메서드를 실행하면 현재 main 쓰레드 내부에서 순차적으로 실행이 된다. 즉, 쓰레드를 실행하는 것이 의미가 없게 된다! 정상적으로 쓰레드를 병렬적으로 실행할려면, .run()메서드가 아니라 .start() 메서드를 실행해야한다.

 

아래 main 메서드를 여러 번 실행해보면 실행할 때마다 결과가 다르게 출력되는 것을 볼 수 있다.

public class Main {
    public static void main(String[] args) {
        MyRunnable r1 = new MyRunnable();
        MyRunnable r2 = new MyRunnable();
        MyRunnable r3 = new MyRunnable();

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        Thread t3 = new Thread(r3);

        t1.start();
        t2.start();
        t3.start();

        for (int a = 1; a < 10; a++) {
            System.out.print("M");
        }
    }
}

 

 

쓰레드 멈춤: sleep()

쓰레드를 멈출려면 Thread.sleep() 메서드를 사용하면 된다. sleep을 구현하려면  try-catch로 InterruptedException을 처리해줘야한다. sleep()은 static의 클래스 메서드이다. 따라서 어떤 쓰레드의 인스턴스를 만들고, main 쓰레드의 실행문 안에서 sleep()을 호출하면 만들어진 쓰레드가 멈추지 않는다. 대신 해당 인스턴스를 감싸고 있는 main 쓰레드가 멈춘다. main 메서드의 클래스 메서드처럼 작용하는 것이다. 이 점에 유의해서 사용해야한다.

 

 

 

2. 쓰레드 사용

 

 

이름

Thread.currentThread.getName() 을 호출하면 쓰레드의 기본 이름이 출력된다. 그리고 Thread의 인스턴스를 만들고 setName() 메서드로 이름을 정해줄 수도 있다.

 

우선순위

 

new Thread().setPriority(Thread.MIN_PRIORITY)

 

라는 식으로 priority라는 int 정수를 인자로 주어 쓰레드 실행의 우선순위를 설정할 수도 있다. 다만 이 메서드는 JVM에게 우선순위 힌트를 줄 뿐, 실제로 어떤 쓰레드가 먼저 실행될지는 OS가 결정하여 처리하기 때문에 이런 기능이 있다는 정도만 알면 된다. Thread.yield()를 통해서 같은 우선순위의 다른 쓰레드에게 양보할 수도 있으나, 이 역시 OS에 의존하여 순서가 지켜지지는 않는다는 점을 기억하자.

 

 

종료 여부 확인

new Thread().isAlive() ? "alive" : "done"

 

isAlive() 메서드로 쓰레드의 실행 여부를 판단할 수 있다.

 

 

종료할 때까지 기다리기

new Thread().join()

 

실행 중인 thread의 인스턴스에 .join()메서드를 실행하도록 하면 해당 쓰레드가 끝날 때까지 현재 쓰레드가 기다리도록 할 수 있다. 강의에서처럼 1개 쓰레드가 for문을 돌며 반복문을 타도록 하고, start()로 실행한다음 main 쓰레드에서 Scanner 입력을 통해 실행 중인 쓰레드.join()을 실행하면 원래 쓰레드의 실행이 끝날 때까지 Scanner를 통한 입력이 불가하게 되는 것을 볼 수 있다.

 

.join()을 호출한 쓰레드가 대상이 되는 쓰레드를 기다리는 것이기도 하고, 대상 쓰레드의 흐름에 호출 쓰레드가 끼어드는 의미라서 join 이라는 이름을 갖는다. 또한 join 메서드의 인자로 시간을 주면, 그 시간 동안만 join 되도록 할 수 있다.

 

 

쓰레드 멈추기

interrupt() 메서드를 사용하면 된다. 다만 호출당하는 스레드에서 catch문에 interruptedException을 구현하고, 해당 예외 발생 시 처리할 코드를 작성해주어야 처리가 되는 형태다. 아래 예제로 이해하는게 빠르다.

public class SingSong implements Runnable{

    int count;

    public SingSong(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        var song = "쓰레드 이름: %s - %d초다 숫자 세는 노래";

        for (var i = 0; i < this.count; i++) {
            try {
                Thread.sleep(2000);
                System.out.printf(song + "\n", Thread.currentThread().getName(), i);
            } catch (InterruptedException e) {
                System.out.println("멈출게요");
                return;
            }

        }
    }
}

 

public class Main {
    public static void main(String[] args) {
        Thread songThread = new Thread(new SingSong(10));

        songThread.setName("쓰레드 이름 바꿨음");

        songThread.start();

        try(Scanner sc = new Scanner(System.in)) {
            while (sc.hasNext()) {
                var line = sc.nextLine();
                System.out.println(line);
                if (line.equalsIgnoreCase("quit")) break;
                if (line.equalsIgnoreCase("stop")) {
                    System.out.println("조용히 시키기");
                    songThread.interrupt();
                }
            }
        }

    }
}

 

 

 

3. 그룹과 데몬

 

쓰레드 그룹

연관된 쓰레드들을 그룹으로 묶기 위해 사용하며, 쓰레드 그룹끼리 포함시키거나 분리시킬 수 있다. 쓰레드 그룹을 따로 지정해주지 않으면 기본적으로 main 쓰레드 그룹에 속한다. 아래 같은 문법으로 사용한다.

 

public void group() {
    ThreadGroup threadGroup = new ThreadGroup("tg");
    String name = threadGroup.getName();

    ThreadGroup threadGroup1 = new ThreadGroup("new_tg");

    // 특정 그룹에 속하는 쓰레드 만들기
    Thread thread = new Thread(threadGroup, () -> {});

    // 다른 그룹에 속하는 쓰레드 그룹 만들기
    ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "new_tg2");
    
    // 그룹 내 실행 중인 쓰레드 | 쓰레드 그룹의 수를 조회한다. 
    threadGroup.activeCount();
    threadGroup.activeGroupCount();
    
    // 그룹 내 쓰레드 일괄 종료
    threadGroup.interrupt();
}

 

 

 

데몬 쓰레드

 

백그라운드 프로세스처럼 주 쓰레드의 작업을 보조하는 역할을 한다. 주 쓰레드가 종료되면 데몬 쓰레드도 종료된다. Runnable을 직접 인자로 받는 형태로 작성하고, 앞에서 짰던 SingSong Thread를 그 쓰레드 내부에서 실행시키면 아래와 같은 결과가 출력된다.

public class Main2 {
    public static void main(String[] args) {
        Thread parentThread = new Thread(() -> {

            Thread singSongThread = new Thread(new SingSong(10));

            singSongThread.start();

            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("잠시 쉬고,");
            }
        });

        parentThread.start();

    }
}

 

 

바깥쪽에서 둘러싼 parentThread가 작업을 완료했음에도("잠시 쉬고,"가 끝남), 계속해서 SingSongThread가 실행된다. parentThread가 실행이 멈추면 내부의 SingSongThread가 멈추게 하기 위해서는 singsongThread.start() 코드 전에 아래 코드를 추가해주면 된다.

singSongThread.setDaemon(true);

 

이후 실행 시 아래와 같은 결과가 나오는 것을 확인할 수 있다.

 

728x90
반응형