본문 바로가기

Programming/JAVA

[자바] 스레드(Thread)에 대하여


멀티스레드와 API에 대하여

 

<스레드란?>

 

  다른 언어와 마찬가지로 자바에서도 스레드(Thread)라는 용어를 사용한다.

  우선 정의를 살펴보면 '시작점과 종료점을 지니는 일련의 하나의 작업 흐름'을 스레드라고 한다.

 

<멀티스레드란?>

 

  그렇다면 멀티 스레드란 무엇인가?

  여러분이 인터넷에서 파일을 다운로드 받아 본 경험이 있을 것이다. 그것이 바로 멀티 스레드라고 할 수 있다.

 

  이러한 멀티스레드는 CPU의 시분할 개념(Time Sharing)의 작동 방식에 근거한다. 시분할 개념이란 프로그램에 정해진

  순서대로 단시간(약 100밀리초)씩 실행 시간을 주어 이를 되풀이 해서 일정 기간에 복수의 프로그램을 실행할 수 있는 시스템,

  즉 시간을 세분화해서 사용하는 한편 동시에 복수의 일을 처리하는 것처럼 보이는 방식이다.

 

<스레드를 구현하는 방법>

 

  (1) java.lang.Thread 클래스를 상속받거나 (2) java.lang.Runnable 인터페이스를 구현한다.

 

  이때, 주의할 점은 Runnable 인터페이스를 구현한 클래스를 사용할 때는 Thread 클래스 객체를 함께 써야한다.

  이유는 Runnable 인터페이스에는 스레드로서 동작하기 위한 메서드들이 구현되어 있지 않기 때문이다.

 

<스레드 상속 작성 방법>

 

  (1) 스레드를 상속받는 클래스를 작성한다

  (2) run() 메서드를 오버라이딩하여 내용부를 구현한다

  (3) main() 메서드 내부에서 스레드를 상속받은 클래스의 객체를 생성한다

  (4) 해당 객체의 start() 메서드를 호출한다

 

<Runnable 구현 작성 방법>

 

  (1) Runnable을 구현하는 클래스를 작성한다

  (2) run() 메서드를 오버라이딩하여 내용부를 구현한다

  (3) main() 메서드에서 Runnable을 구현한 클래스의 객체를 생성한다.

  (4) Thread 객체를 생성하여 매개 변수로 3번의 객체를 대입한다

  (5) Thread 객체의 start() 메서드를 호출한다.

 

<Thread를 상속받는 형태와 Runnable을 구현하는 형태, 2가지로 만들었을까?>

 

  이유는 자바가 다중 상속이 불가능하기 때문이다.

  따라서 어떤 클래스를 상속받은 클래스를 스레드화시켜야 할 필요가 있을 때에는 Runnable과 같은 인터페이스가 꼭 필요하다.

 

<스레드의 API를 살펴보자>

 

  살펴볼 API는 (1)Runnable 인터페이스와 (2)Thread 클래스이다.

  그러나 Runnable 인터페이스는 public void static run() 메서드 하나 밖에 없으므로 생략하기로 한다.

 

Thread Class

 

(1) Constructs

Construct

내 용

Thread()

기본 Thread 객체

Thread(String name)

특정 이름을 가진 Thread 객체

Thread(Runnable target

target을 실행 스레드로 가지는 객체

Thread(Runnable target, String name)

target을 싫애 스레드로 가지고 특정 이름을 가지는 객체

 

(2) Methods

Return-type Method

내 용

static int activeCount()

현재 활동 중인 스레드 개수

static Thread currentThread()

현재 시점에 수행 중인 Thread 객체

static int enumerate(Thread[] tarray)

현재 활동 중인 모든 스레드를 tarray 배열에 저장

String getName()

스레드 이름 얻음

int getPriority

스레드 우선순위 값 얻음

void interrupt()

스레드를 멈춤

static boolean interrupt()

현재 스레드가 인터럽트(interrupt)되었는지 확인

boolean isAlive()

스레드가 살아 있는지 확인

boolean isDaemon()

데몬(Daemon) 스레드인지 확인

boolean isInterrupted()

스레드가 인터럽트 되었는지 확인

void join()

다른 스레드를 멈추고 이 스레드를 끝까지 실행

void join(long sec)

다른 스레드를 멈추고 이 스레드를 sec밀리초만큼 실행

void setDaemon(boolean bool)

스레드의 데몬 여부 결정

void setName(String name)

스레드의 이름 값 설정

void setPriority(int pri)

스레드의 우선 순위 설정 (1~10)

static void sleep(long sec)

sec밀리초만큼 현재 실행 중인 스레드 멈춤

void start()

스레드 실행 메서드

static void yield()

현재 실행 중인 스레드를 멈추고 다음 계획된 스레드를 실행

 

  다 읽어보았다면, Daemon이라는 녀석이 무엇인지 궁금할 것이다. 있다가 설명하도록 하겠다.

 

<스레드 예제 : Runnable을 구현하는 클래스>

 

  이 예제를 테스트할 때에는 여러 번 실행을 시켜서 run() 메서드의 내용부가 어디에 출력되는지 확인해 봐야 한다.

  왜냐하면 스레드는 매번 실행 위치가 똑같지 않기 때문이다.

 

  매번 실행할때마다 조금씩 다른 결과를 가져올 것이다.

 

  class A implements Runnable { // Runnable 인터페이스를 상속받았다.
      public void run() {
          System.out.println("Here is My Thread");
          for (int i = 0; i < 100; i++) {
              for (char ch = 'A'; ch <= 'z'; ch++) {
                  System.out.print(ch);
              }
          }
      }
  }

  public class Java {
      public static void main(String[] args) {
          System.out.println("Main Thread");
          A a = new A(); // 객체 생성
          Thread th = new Thread(a); // Thread 객체 생성 // Runnable 인터페이스에는 스레드를 구현할 수 있는 메서드가 없으므로
          th.start(); // Thread 실행 // run()을 하지 않고 start()를 하는 이유는 일반 메서드와 구분이 되지 않기 때문에 차별을 위해
          for (int i = 1; i < 1000; i++) {
              System.out.print(i);
              if (i % 10 == 0) System.out.println();
              else System.out.print("\t");
          }
          System.out.println("Main Thread Destroy");
      }
  }

 

  출력결과를 보면 알파벳이 100번 출력되어야 하겠지만, 중간 중간 숫자가 섞여있다.

  이런 이유는 main 스레드와 A 스레드가 시분할 개념으로 CPU의 제어권을 가로채기 때문이다.

 

<스레드를 상속받은 클래스>

 

  Thread 클래스를 바로 상속받았기 때문에 또다시 스레드의 생성자를 이용하여 담아 줄 필요가 없다.

  바로 start라고 하여 main 스레드와 동급인 또 하나의 스레드를 시작할 수 있다.

 

  class A extends Thread { // 이번엔 Runnable 인터페이스가 아닌 Thread를 상속받았다.
      public void run() {
          System.out.println("Here is My Thread");
          for (int i = 0; i < 100; i++) {
              for (char ch = 'A'; ch <= 'z'; ch++) {
                  System.out.print(ch);
              }
          }
      }
  }

  public class Java {
      public static void main(String[] args) {
          System.out.println("Main Thread");
          A a = new A(); // 객체 생성
          // Thread th = new Thread(a); // 상속 받았으므로 이제 이 부분은 제외한다.

          a.start(); // 상속 되었으므로 바로 start하면 되겠다.
          for (int i = 1; i < 1000; i++) {
              System.out.print(i);
              if (i % 10 == 0) System.out.println();
              else System.out.print("\t");
          }
          System.out.println("Main Thread Destroy");
      }
  }

 

<스레드의 우선순위 값 확인>

 

  이러한 스레드는 CPU의 시분할 개념을 이용하여 여러 개의 작은 프로그램들이 번갈아 가면서 실행할 수 있기 때문에

  어떤 것을 우선적으로 실행시켜야 하는지 혹은 어떤 프로그램을 다른 것보다 좀더 오래 실행시켜야 하는지에 대한 정의가 필요하다.

 

  바로 그러한 것을 정해주는 값이 우선순위 값이다. 이것은 자바의 스레드에서 숫자로 표시되며 그 범위는 1~10이다.

  여기서는 10이 가장 우선순위가 제일 높은 것이다. 일반적으로 프로그램을 만들어 바로 실행시키면 우선순위의 값은

  NORM_PRIORITY의 값인 5를 가지게 된다. 다음 예제는 우선순위를 표시하는 필드 값을 출력해 본 것이다.

 

  public class Java {
      public static void main(String[] args) {
          int max = Thread.MAX_PRIORITY;
          int norm = Thread.MIN_PRIORITY;
          int min = Thread.NORM_PRIORITY;
          System.out.println("최고 우선 순위 값 = " + max);
          System.out.println("기본 값 = " + norm);
          System.out.println("최저 우선 순위 값 = " + min);
      }
  }

 

<스레드의 우선순위 정하기>

 

  다음은 Runnable 인터페이스와 스레드를 구현한 클래스이다. 각 스레드의 우선순위를 적절하게 조절하고 실행시켜 보자.

 

  class A extends Thread {
      public void run() {
          System.out.println("Here is a Class A");
      }
  }

  class B implements Runnable {
      public void run() {
          System.out.println("Here is a Class B");
      }
  }

  public class Java {
      public static void main(String[] args) {
          A a = new A();
          Thread b = new Thread(new B());

          a.setPriority(5); // 우선순위를 정해보자 // setPriority가 도와줄 것이다.
          b.setPriority(10);

          a.start(); // 어떠한가? 바뀌었는가?
          b.start(); // 지금은 CPU가 먼저 출발 명령을 받은 것부터 실행하기에 별 변화가 없다. 일단 정할 수 있다는 것만 알고 가자.
      }
  }

 

<Thread API의 활용>

 

  앞에서 메서드들을 대략 알아보았고, 또한 Documnet API에 대해 알아보았다. 이번에는 몇몇 메서드가 직접 활용되는 것을 보자.

 

  public class Java {
      public static void main(String[] args) {
          int count = Thread.activeCount(); // 현재 진행 중인 스레드의 갯수를 확인할 수 있다.
          System.out.println("count = " + count);
          Thread cur = Thread.currentThread(); // 이 코딩을 만나는 순간, 실행 중인 스레드를 객체로 담아둘 수 있다.
          System.out.println("cur = " + cur);
          System.out.println("cur name = " + cur.getName()); // 객체에 담겨져 있는 스레드의 이름을 출력한다.
          try {
              Thread.sleep(2000); // 시간 지연 메서드로 모든 스레드를 2초간 멈춘다. 이때 다른 외부의 스레드에 의한 침범이나

                                           // 명령에 의해 스레드가 종료되는 상태를 확인하기 위해 InterruptedException이라는 예외처리
          } catch (InterruptedException ie) {
              System.out.println("이것이 실행되는 경우는 없음");
          }
          System.out.println("cur priority = " + cur.getPriority()); // 객체에 담겨 있는 우선 순위를 출력한다.
          System.out.println("cur alive = " + cur.isAlive()); // 객체가 아직 유효한지 출력한다.
          System.out.println("cur daemon = " + cur.isDaemon()); // 객체가 main 스레드에 종속적인 스레드인지 아닌지 확인
      }
  }

 

<스레드의 종류>

 

  스레드에는 두 종류가 있다. 바로 독립 스레드와, 종속 스레드가 그것이다.

 

  (1) 독립스레드 : main() 메서드와 무관하게 계속 실행하는 형태

  (2) 종속스레드 : 자신의 실행이 끝나지 않더라도, main과 함께 종료되는 형태

 

   그리고 종속스레드가 바로 Daemon 스레드인 것이다.

   위에서도 보았지만, setDaemon(true)로 설정하게 되면 종속스레드, 다시 말해 데몬스레드가 된다.

 

<단원 마무리>

 

  혹 부족하다거나, 필요한 내용은 얘기하는대로 반영하도록 하겠다.