끄적이기

[TIL - 11] 쓰레드(Thread) 정리

배씌 2025. 3. 19. 10:15

 

 쓰레드는 프로세스 내에서 실행되는 여러 개의 실행 단위로, 경량화 프로세스(Lightweight Process, LWP)라고 부르기도 한다. 따라서, 한 프로세스 내 에서 여러개의 쓰레드를 실행할 수 있고, 프로세스 내의 주소 공간이나 자원을 공유할 수 있다. 마지막으로, 각각의 쓰레드는 독립적인 작업을 수행함으로 각자의 스택과 레지스터 셋을 가지고 있다.


쓰레드의 구현과 실행

 

쓰레드를 구현하는 방법에는 두 가지가 있다.

  1. Thread 클래스를 상속받아 구현
  2. Runnable 인터페이스를 구현하는 방법

위 두 가지 방법 모두 run() 메서드를 오버라이딩 하여 몸체를 작성하면 된다.

 

1. Thread 클래스

Thread 클래스를 상속받아 run() 메서드를 오버라이딩하고, 객체를 생성하여 start() 메서드를 호출하여 실행시킨다.

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 실행 중");
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

 

2. Runnable 인터페이스 구현

Runnable 인터페이스를 implements 하여 run() 메서드를 오버라이딩 하고, Thread(Runnable target) 생성자로 Thread를 생성하면 된다.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 실행 중");
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
    	Runnable r1 = new MyRunnable();
        Thread t1 = new Thread(r1);
        t1.start();
    }
}

 

Runnable 인터페이스 방식을 더 선호한다.

 Java에서는 다중상속이 불가능하기 때문에, Thread 클래스를 직접 상속받아 작성하는 것 보다, 단순 인터페이스를 구현하는 것이 다중 상속에 유리하다. 또한, 아래와 같이 재사용성도 높기 때문에 Runnable 방식을 더 선호한다.

   Runnable r = new MyRunnable();
   Thread t1 = new Thread(r);
   Thread t2 = new Thread(r);
   t1.start();
   t2.start();

 

단, 위와 같은 방식은 동기화 문제가 발생 가능하다. 여러개의 쓰레드가 같은 Runnable 객체를 실행하면, 객체 내부의 필드를 여러 쓰레드가 동시에 변경할 위험이 있다. 이는 synchronized 키워드를 통해 해결 가능한데, 추후 따로 다루겠다.

 

Thread 클래스를 사용해야 하는 때는 언제인가?

클래스를 상속한다는 것은 새로운 기능을 추가하거나, 개선 및 수정 한다는 것을 의미한다. 따라서 Thread 의 어떠한 기능을 추가하거나 수정해야한다면 Thread 클래스를 상속해서 사용하자. (일반적으로는 Runnable 인터페이스 사용)


 

Main Thread

 

우리가 자바 프로그램을 실행하면 JVM이 기본적으로 하나의 쓰레드를 실행하는데, 이 쓰레드가 바로 메인 쓰레드이다. 우리가 기계적으로 사용하는 public static void main(String[] args) 메서드를 실행하고, 전체적인 자바 프로그램의 생명 주기를 관리한다.

 

우리가 쓰레드를 구현하여 실행한다면, 사실 프로그램 내에서는 2개의 쓰레드가 실행되는 것이다. (메인 쓰레드, myThread)

 

 

쓰레드의 상태 (Thread Life Cycle)

 

쓰레드의 전반적인 흐름

  1. 쓰레드를 생성하면 new 상태
  2. start() 메서드를 호출하면 실행 대기(Runnable) 상태가 된다.
  3. 실행 대기 상태에서 스케줄러에 의해 차례대로 실행
  4. 실행 시간이 다 되거나, yield() 가 호출되면 다시 실행 대기 상태가 된다.
  5. 실행 중에 wait(), sleep(), join() 등에 의해 일시 정지 상태(Wait) 가 될 수 있다.
  6. 일시 정지 시간이 다 되거나 notify(), notifyAll(), interrupt() 메서드가 호출되면 다시 실행 대기 상태가 된다.
  7. 실행을 모두 마치면 소멸(Terminate) 된다.

 

New

- 쓰레드가 실행 준비를 완료한 상태, start() 메서드 호출 전 상태

 

Runnable

- start() 가 호출되어 실행될 수 있는 상태

- 쓰레드 스케줄링에 따라 CPU에서 실행 대기 중인 상태

 

Wait

- 다른 쓰레드에서 깨워줄 때 까지(notify() 등) 기다리는 상태

 

Blocked

- 사용하고자 하는 객체의 잠금(lock)이 풀릴 때까지 대기하는 상태

 

Terminated

- 실행이 종료된 상태


쓰레드 상태 변경 메서드

sleep() 메서드

 지정 시간 동안 쓰레드를 멈추게 한다. 지정된 시간이 다 되거나 interrupt() 가 호출되면 InterruptedException 이 발생하고, 실행 대기 상태가 된다. 따라서, 항상 try-catch 문으로 예외를 처리해줘야 한다.

void delay(long millis) {
    try {
    	Thread.sleep(millis);
    } catch (InterruptedException e ) {}
}

 

sleep() 은 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에, 참조 변수가 아닌 "Thread.sleep()" 과 같이 static 메서드를 사용해야 한다.

 

join() 메서드

join() 메서드를 호출한 쓰레드가 종료될 때까지 메인 쓰레드가 기다린다.

public class JoinExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyThread("Thread 1"));
        Thread thread2 = new Thread(new MyThread("Thread 2"));
        Thread thread3 = new Thread(new MyThread("Thread 3"));

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join(); // thread1이 종료될 때까지 대기
            thread2.join(); // thread2가 종료될 때까지 대기
            thread3.join(); // thread3이 종료될 때까지 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("메인 스레드 종료");
    }
}

Interrupt 발생 시 쓰레드 상태 변화

1. 해당 쓰레드가 sleep(), wait(), join() 같은 대기 상태(Wait) 였을 경우 -> InterruptException 발생, 깨어남

 

2. 해당 쓰레드가 실행 중인 경우 -> 즉시 예외가 발생하지는 않음

 

3. 해당 쓰레드가 Blocked 상태인 경우 -> 즉시 예외가 발생하지는 않음