자바 - 스레드(Thread)

업데이트:

java-logo

스레드(Thread)

  • 스레드란?
  • “실”이란 뜻을 가진다.
  • 하나의 실은 하나의 끊기지 않은 실행 흐름을 뜻한다.

할당된 메모리 공간을 기반으로 실행 중에 있는 프로그램을 가리켜 프로세스(Process)라고 한다. 따라서 프로세스를 간단히 ‘실행중인 프로그램’으로 설명하기도 한다. 여태 내가 공부해왔던 프로세스들은 프로그램 흐름을 main 메소드하나만 형성하고 있었다.

그러나 하나의 프로세스 내에서 둘 이상의 프로그램 흐름을 형성하고 싶다면, Thread 클래스에 분기문을 작성하면 된다.

멀티태스킹(multi-tasking)

한 개의 CPU가 여러 코드를 동시에 실행하는 것.

실제로는 일정한 시간을 쪼개 CPU가 코드를 왔다갔다하며 실행한다. 동시에 실행되는 것 처럼 보이는 것은 CPU의 속도가 빠르기 때문이다.

즉 한정된 자원(cpu)을 사용하여 CPU스케줄링 알고리즘(문제해결방법)에 따라 여러개의 프로세스(APP)를 동시에 실행하는 것!

구현방법

  • 멀티 프로세싱(multi - processing) : 프로세스를 여러개 복제하여 실행
    • 단순히 복제(fork)하기때문에 구현(코딩)이 쉽다.
    • 메모리를 그대로 복제한다. 그래서 메모리 낭비가 심하다.
    • 자식 프로세스는 부모에 종속되지 않는다. (부모프로세스가 종료되어도 자식은 종료되지 않음.)
  • 멀티 스레딩(multi - threading) : 프로세스의 작업 중 멀티태스킹이 필요한 작업만 분리(스레드)하여 여러개 실행
    • 스레드 생성 -> 구현이 조금 더 복잡하다.
    • 프로세스의 Heap을 공유한다. -> 메모리를 절약한다.
    • 스레드는 프로세스에 종속된다. -> 프로세스 종료 = 스레드 종료

CPU scheduing : 여러개의 프로세스를 동시에 실행하기 위해 cpu사용을 관리하는 방법

  1. Round-Robin : 각 프로세스에 균등한 실행시간을 부여하는 것
    • Windows OS
  2. Priority + Aging
    • 우선순위가 높은 프로세스에 CPU사용권한을 더 많이 배분한다.(더 자주실행된다.)
    • 단 우선순위가 낮아서 실행이 연기될 때 마다 우선순위 레벨(age)을 높여서 결국 실행하게 하는 방법 -> 에이징 기법(aging)
    • Linux, macOS, Unix

컨텍스트 스위칭(context switching)

동시에 여러 개의 프로세스나 스레드를 실행할 때 CPU 사용권을 뺏어 다른 프로세스나 스레드에게 주기 전에 현재까지 실행한 코드의 위치 정보를 저장해야 한다. 또한 CPU 사용권을 주기 전에 그 프로세스나 스레드가 이전에 어디까지 실행했었는지 이전 실행 위치 정보를 로딩해야 한다. 즉 실행 위치에 대한 정보를 저장하고 로딩하는 것을 말한다.

  • 스레드에는 우선순위가 존재하지만, Windows OS의 경우 우선순위값이 실행 순서나 실행 회수에 큰 영향을 주지 않는다. ? OS에 따라 실행 정책이 다르기 때문이다.
  • 그냥 특정 코드를 동시에 실행하고 싶을 때 스레드를 사용한다고 생각하자!

Java 의 캐치프레이즈가 “Write Once, Run Anywhere!” 이다. 즉 OS에 상관없이 동일하게 동작하게 만드는 것이 자바의 목적이다. 그런데 우선 순위에 따라 실행률이 달라지고, OS 마다 차이가 난다면, 자바의 목적에 부합하는 것이 아니다. 그래서 가능한 OS에 영향을 덜 받는 방식으로 코딩해야 한다. 이런 이유로 스레드를 다룰 때 우선 순위를 고려하는 방식으로 프로그래밍을 하지 말라!

참고 스레드의 이름 지정하기

Thread 클래스에는 이름의 지정을 위한 생성자가 다음과 같이 정의되어 있다.

public Thread(String name)

키워드 super를 이용해 상위 클래스의 생성자를 호출할 수 있다. 이를 이용해서 스레드의 이름이 지정가능하며, 이렇게 지정된 이름은 getName메소드를 통해 문자열의 형태로 참조할 수 있다.

스레드를 만드는 방법

1) Thread를 상속받기

특징

  • 구현하기 편하다
  • 다중 상속이 불가능하기 때문에 다른 클래스를 상속 받을 수 없다.
  • 즉 다른 클래스를 상속받으면서 스레드가 될 수 없다.
  • 기존의 스레드에서 분리해서 새 스레드에서 실행하고픈 코드가 있다면, run()을 재정의하여 그 메서드에 해당 코드를 두면 된다.

2) Runnable로 구현하기

특징

  • 실무에서 많이 사용한다.
  • 인터페이스를 구현하는 것이기 때문에 다른 클래스를 상속 받을 수 있다.
  • 직접적으로 스레드가 아니기 때문에 실행할 때는 Thread의 도움을 받아야 한다.

예제 - 로컬 클래스로 구현하기

class MyRunnable implements Runnable {
      @Override
      public void run() {
        for(int i = 0; i < 1000; i++) {
          System.out.println("===> "+ i);
        }
      }
    }
    // 스레드 실행하기
    // => Runnable 구현체를 Thread 객체에 실어서 실행한다.
    // => start()를 호출하여 기존 스레드에서 분리하여 스레드를 실행시킨다.
    Thread t = new Thread(new MyRunnable());
    t.start();

예제 - 익명 클래스로 구현하기

new Thread(new Runnable() {
      @Override
      public void run() {
        for(int i = 0; i < 1000; i++) {
          System.out.println("===> "+ i);
        }
      }
    }).start();

예제 - 람다로 구현하기

new Thread(() -> {
        for(int i = 0; i < 1000; i++) {
          System.out.println("===> "+ i);
        }
      }).start();

스레드의 라이프 사이클(lifecycle)

  • Running상태 : CPU를 받아서 실행 중이거나 CPU를 받을 수 있는 상태.
  • Not Runnable 상태 : CPU를 받지 않는 상태
  • Dead상태 : run()메소드 실행이 완료되면 해당 스레드는 dead상태가 된다. dead상태가 된 스레드는 다시 runnable상태가 되지 못한다.
스레드의 생명주기
new Thread()    start()              sleep()/wait()
   준비 -------------------> Running ---------------> Not Runnable
                            ^  |    <---------------
                            |  |    timeout/notify()
                            X  |
                            |  |  run() 메서드 종료
                            |  V
                            Dead

스레드 동기화(Synchronization)

다음과 같이 은행계좌에서 돈을 인출하는 withdraw메소드가 있다고 가정하자.

이 메서드처럼 여러 스레드가 같은 메모리(balance 필드)의 값을 동시에 변경하는 경우 문제가 발생하는 코드를 “크리티컬 섹션(임계영역, critical section)” 또는 “크리티컬 리전(critical region)”이라고 부른다.

public class Account {
  String accountId;
  long balance;

  public Account(String accountId, long balance) {
    this.accountId = accountId;
    this.balance = balance;
  }

public long withdraw(long money) {
    long b = this.balance;
    b -= money;

    if(b < 0)
      return 0;
    this.balance = b;
    return money;
  }
 }

한 번에 한 스레드 만이 호출하도록 접근을 제한하고 싶다면 메서드 전체를 동기화 블록으로 선언하면 된다. 어떻게? 메서드 앞에 synchronized를 붙인다.

synchronized public long withdraw(long money) {}

크리티컬 섹션에 동시에 접근하지 못하게 하는 기법을 뮤텍스(mutex)또는 세마포어(semaphore)라고 부른다. 자바에서 뮤텍스를 구현하는 방법은 크리티컬 섹션에 해당하는 메서드나 코드 블록에 synchronized키워드를 붙여 한번에 한 스레드만 진입할 수 있도록 lock을 건다.

스레드 재사용 - wait(), notify()

  • wait()와 notify()메소드는 동기화 처리를 해서 한 순간에 하나의 스레드만 호출이 가능하도록 해야한다.

  • 동기화 영역이란? synchronized로 선언된 메서드 또는 블록
  • 스레드를 시작하자마자 wait()로 작업 지시를 기다리게 한다.
  • 그 다음 notify()를 통해 기다림이 끝났다는 것을 알림 받아야 한다.
package com.eomcs.concurrent.ex6;

import java.util.Scanner;

public class Exam0140 {

  public static void main(String[] args) {

    class MyThread extends Thread {
      int count;

      public void setCount(int count) {
        this.count = count;

        synchronized (this) {
          // synchnorized 블록에서 지정한 객체의 사용을 기다리는 스레드에게
          // 작업을 시작할 것을 알린다.
          notify();
          // notify()도 동기화 영역에서 호출해야 한다.
          // 안그러면 IllegalMonitorStateException 예외가 발생한다.
        }
      }

      @Override
      public void run() {
        synchronized (this) {
          System.out.println("스레드 시작했음!");
          try {
            while (true) {
              System.out.println("스레드 대기중...");
              wait();
              System.out.println("카운트 시작!");
              for (int i = count; i > 0; i--) {
                System.out.println("==> " + i);
                Thread.sleep(1000);
              }
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    }

    MyThread t = new MyThread();
    t.start();

    Scanner keyScan = new Scanner(System.in);

    while (true) {
      System.out.print("카운트? ");
      String str = keyScan.nextLine();
      if (str.equals("quit")) {
        break;
      }

      int count = Integer.parseInt(str);
      t.setCount(count);
    }

    System.out.println("main 스레드 종료!");
    keyScan.close();
  }
}


댓글남기기