스레드(Thread)

라이언양 위키
둘러보기로 가기 검색하러 가기

1 소개

2 프로세스

운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스(process)라고 부릅니다. 사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 이것이 프로세스입니다.

3 멀티 태스킹

운영체제는 두 가지 이상의 작업을 동시에 처리하는 멀티 태스킹(multi tasking)을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고 병렬로 실행시킵니다.

멀티 태스킹은 꼭 멀티 프로세스를 뜻하는 것은 아닙니다. 한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션도 있습니다. 어떻게 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있을까요? 그 비밀은 멀티 스레드(multi thread)에 있습니다.

4 스레드

스레드(thread)는 사전적 의미로 한 가닥의 실이라는 뜻인데, 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름입니다. 하나의 스레드는 하나의 코드 실행 흐름이기 때문에 한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이 생긴다는 의미입니다.

멀티 프로세스는 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세스는 서로 독립적입니다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않습니다. 하지만 멀티 스레드는 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않습니다. 하지만 멀티 스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자첵 종료될 수 있어 다른 스레드에 영향을 미치게 됩니다.

5 메인 스레드

자바의 모든 애플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작합니다. 메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행이 종료됩니다.

메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있습니다. 즉, 멀티 스레드를 생성해서 멀티 태스킹을 수행합니다.

싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료됩니다. 하지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않습니다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않습니다.

6 작업 스레드 생성과 실행

멀티 스레드로 실행하는 애플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 합니다.

어떤 자바 애플리케이션건 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 됩니다. 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요합니다. java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만, Thread 클래스를 상속해서 하위 클래스를 만들어 생성할 수도 있습니다.

7 Thread 생성 방법

7.1 1. Thread 클래스로부터 직접 생성

java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 을 매개값으로 갖는 생성자를 호출해야 합니다.

Thread thread = new Thread(Runnable target);

Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름입니다. Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 합니다. Runnable 에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run() 을 재정의해서 작업 스레드가 실행할 코드를 작성해야 합니다.

class Task implements Runnable {
  public void run() {
    스레드가 실행할 코드;
  }
}

Runnable은 작업 내용을 가지고 있는 객체이지 실제 스레드가 아닙니다. Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출해야 비로소 작업 스레드가 생성됩니다.

Runnable task = new Task();

Thread thread = new Thread(task);

코드를 좀 더 절약하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있습니다. 오히려 이 방법이 더 많이 사용됩니다.

Thread thread = new Thread(new Runnable() {
  public void run() {
    스레드가 실행할 코드;
  }
}

작업 스레드는 생성 즉시 실행되는 것이 아니라, start() 메소드를 다음과 같이 호출해야만 비로소 실행됩니다.

thread.start();

start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리합니다.

import java.awt.Toolkit;

public class BeepPrintThread {
  public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();

        for (int i = 0; i < 5; i++) {
          toolkit.beep();
          try { Thread.sleep(500); } catch(Exception e) {}
        }
      }
    });
    thread.start();

    for (int i = 0; i < 5; i++) {
      System.out.println("띵");
      try { Thread.sleep(500); } catch(Exception e) {}
    }
  }
}

7.2 2. Thread 하위 클래스로부터 생성

작업 스레드가 실행할 작업을 Runnable 로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있습니다.

다음은 작업 스레드 클래스를 정의하는 방법인데, Thread 클래스를 상속한 후 run() 메소드를 재정의(overriding)해서 스레드가 실행할 코드를 작성하면 됩니다. 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성ㅎ는 방법과 동일합니다.

public class WorkerThread extends Thread {
  @Override
  public void run() {
    스레드가 실행할 코드;
  }
}

코드를 좀 더 절약하기 위해 다음과 같이 Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있습니다.

Thread thread = new Thread() {
  public void run() {
    스레드가 실행할 코드;
  }
};

앞의 예제를 Thread 하위 클래스로부터 생성하는 방법으로 수정한 코드는 아래와 같다.

import java.awt.Toolkit;

public class BeepThread extends Thread {
  @Override
  public void run() {
    Toolkit toolkit = Toolkit.getDefaultToolkit();

    for (int i = 0; i < 5; i++) {
      toolkit.beep();
      try { Thread.sleep(500); } catch(Exception e) {}
    }
  }
}
public class BeepMainThread {
  public static void main(String[] args) {
    Thread thread = new BeepThread();
    thread.start();

    for (int i = 0; i < 5; i++) {
      System.out.println("띵");
      try { Thread.sleep(500); } catch(Exception e) {}
    }
  }
}

8 스레드의 이름

스레드는 자신의 이름을 가지고 있습니다. 스레드의 이름이 큰 역할을 하는 것은 아니지만, 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용됩니다.

메인 스레드는 'main' 이라는 이름을 가지고 있고, 우리가 직접 생성한 스레드는 자동적으로 'Thread-n' 이라는 이름으로 설정됩니다. n은 스레드의 번호를 말하는데, Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경하면 됩니다.

thread.setName("스레드 이름");

반대로 스레드 이름을 알고 싶을 경우에는 getName() 메소드를 호출하면 됩니다.

thread.getName();

setName()과 getName()은 Thread 클래스의 인스턴스 메소드이므로 스레드 객체의 참조가 필요합니다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread 클래스의 정적 메소드인 currentThread()를 이용해서 현재 스레드의 참조를 얻을 수 있습니다.

Thread thread = Thread.currentThread();

9 동기화 스레드

싱글 스레드 프로그램에서는 1개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있습니다. 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야 합니다.

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 합니다. 자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드를 제공합니다. 스레드가 객체 내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 합니다.

동기화 메소드를 만들려면 다음과 같이 메소드 선언에 synchronized 키워드를 붙이면 되는데, 인스턴스와 정적 메소드 어디든 붙일 수 있습니다.

public synchronized void method() {
  임계 영역; // 단 하나의 스레드만 실행
}

동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀립니다.

만약 동기화 메소드가 여러 개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드도 실행할 수 없습니다. 하지만 이때 다른 스레드에서 일반 메소드는 실행이 가능합니다.

10 스레드 상태

스레드 객체를 생성하고 start() 메소드를 호출하면 바로 실행되는 것이 아니라 실행 대기 상태가 됩니다. 실행 대기 상태란 언제든지 실행할 준비가 되어 있는 상태를 말합니다. 운영체제는 실행 대기 상태에 있는 스레드 중에서 하나를 선택해서 실행 상태로 만듭니다.

실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 다시 실행 대기 상태로 돌아갈 수 있으며, 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 되기도 합니다.

실행 상태에서 run() 메소드의 내용이 모두 실행되면 스레드의 실행이 멈추고 종료 상태가 됩니다.

10.1 실행 대기(waiting) 상태

스레드 객체를 생성하고 start() 메소드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만 사실은 실행 대기 상태가 됩니다. 실행 대기 상태란 실행을 기다리고 있는 상태를 말합니다.

10.2 실행(running) 상태

실행 대기 상태에 있는 스레드 중에서 운영체제는 하나의 스레드를 선택하고 CPU(코어)가 run() 메소드를 실행하도록 합니다. 그때를 실행(running) 상태라고 합니다.

실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 다시 실행 대기 상태로 돌아갈 수 있습니다. 그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 됩니다.

10.3 종료(terminated) 상태

이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행합니다. 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 됩니다. 이 상태를 종료(terminated) 상태라고 합니다.

이처럼 스레드는 실행 대기 상태와 실행 상태로 번갈아 변하면서, 경우에 따라서 실행 상태에서 일시 정지 상태로 가기도 합니다. 일시 정지 상태는 스레드가 실행할 수 없는 상태입니다. 일시 정지 상태에서는 바로 실행 상태로 돌아갈 수 없고, 일시 정지 상태에서 빠져나와 실행 대기 상태로 가야 합니다.

11 스레드 상태 제어

실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 합니다.

11.1 interrupt()

일시 정지 상태의 스레드에서 InterruptedException 을 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다.

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException을 발생시키는 역할을 합니다. 이를 이용하면 run() 메소드를 정상 종료할 수 있습니다.

주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException이 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 Interrupted Exception이 발생한다는 것입니다. 따라서 스레드가 일시 정지 상태가 되지 않으면 interrup() 메소드 호출은 아무런 의미가 없습니다.

11.2 sleep(long millis)

주어진 시간 동안 스레드를 일시 정지 상태로 만듭니다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다.

try {
  Thread.sleep(1000);
} catch(InterruptedException e) {
  // interrupt() 메소드가 호출되면 실행
}

11.3 stop()

(DEPRECATED) 스레드를 즉시 종료합니다. 불안전한 종료를 유발하므로 사용하지 않는 것이 좋습니다.

12 데몬 스레드

데몬(daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드입니다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료되는데, 그 이유는 주 스레드의 보조 역할을 수행하므로 주 스레드가 종료되면 데몬 스레드의 존재 의미가 사라지기 때문입니다. 이 점을 제외하면 데몬 스레드는 일반 스레드와 큰 차이가 없습니다.

데몬 스레드의 적용 예는 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 쓰레기 수집기 등이 있는데, 이 기능들은 주 스레드(워드프로세서, 미디어 플레이어, JVM)가 종료되면 같이 종료됩니다.

스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출해주면 됩니다. 아래 코드를 보면 메인 스레드가 주 스레드가 되고, AutoSaveThread가 데몬 스레드가 됩니다.

public static void main(String[] args) {
  AutoSaveThread thread = new AutoSaveThread();
  thread.setDaemon(true);
  thread.start();
  ...
}

주의할 점은 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출 전에 setDaemon(true)를 호출해야 한다는 것입니다.

참고로 현재 실행 중인 스레드가 데몬 스레드인지 아닌지를 구별하려면 isDaemon() 메소드의 리턴값을 조사해보면 됩니다. 데몬 스레드일 경우 true를 리턴합니다.