[Java] 22. 스레드(Thread)를 사용하는 방법


Study/Java  2020. 5. 25. 17:57

안녕하세요. 명월입니다.


이 글은 Java에서의 스레드(Thread)를 사용하는 방법에 대한 글입니다.


스레드(Thread)는 프로세스 안에서 움직이는 가장 최소 단위의 실행 단위라고 정의되어 있습니다.

먼저 프로세스라는 것은 우리가 프로그램을 실행해서 종료할 때까지의 일련의 처리 로직을 뜻합니다. 그런 프로세스 안에는 여러 개의 스레드를 가질 수 있습니다. 그 중 스레드는 각 독립된 흐름의 처리라고 할 수 있습니다. 설명이 어렵네요..


먼저 쉽게 반복문을 통해서 1-10까지의 콘솔에 값을 출력하는 프로그램을 작성하겠습니다.

public class Example {
  // 실행 함수
  public static void main(String[] args) {
    // 1부터 10까지 반복문
    for (int i = 1; i <= 10; i++) {
      // 콘솔 출력
      System.out.println("i - " + i);
    }
    // 개행
    System.out.println();
    // 1부터 10까지 반복문
    for (int i = 1; i <= 10; i++) {
      // 콘솔 출력
      System.out.println("i - " + i);
    }
  }
}

위는 실행 함수에서 for문을 실행해서 콘솔 출력을 하고 다시 for문을 실행해서 콘솔 출력을 합니다.

전혀 문제 없는 소스입니다.

그런데 이것을 순서대로 처리하고 싶지 않고 동시에 실행하고 싶습니다. 이럴 때 스레드를 사용합니다.

// Runnable 상속 받은 클래스
class Func implements Runnable {
  @Override
  public void run() {
    // 1부터 10까지 반복문
    for (int i = 1; i <= 10; i++) {
      // 콘솔 출력
      System.out.println("i - " + i);
    }
  }
}
public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // 스레드 할당
      Thread th = new Thread(new Func());
      // 스레드 실행
      th1.start();
      // 1부터 10까지 반복문
      for (int i = 1; i <= 10; i++) {
        // 콘솔 출력
        System.out.println("i - " + i);
      }
    } catch (Throwable e) {
      // 에러 발생하면 콘솔 출력
      e.printStackTrace();
    }
  }
}

결과를 보시면 콘솔 출력이 순서대로 나오는 것이 아니고 Thread의 run 함수와 for이 동시에 실행되는 것을 확인할 수 있습니다.


스레드를 사용하면 여러가지 처리를 동시에 처리할 수 있습니다.

public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // i는 2부터 9까지 반복
      for (int i = 2; i < 10; i++) {
        // 배수 설정 (클로져)
        final int multiple = i;
        new Thread(() -> {
          // 합산 변수
          int sum = 0;
          // 0부터 99까지
          for (int j = 0; j < 100; j++) {
            // 배수일 경우
            if (j % multiple == 0) {
              // 합산 변수에 값을 더한다.
              sum += j;
            }
          }
          // 결과 출력
          System.out.println(multiple + " mutiple = " + sum);
        }).start();
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 예제는 2부터 9까지의 배수의 합을 출력하는 예제입니다. 이 정도의 처리식은 굳이 Thread를 사용하지 않아도 빠르게 처리됩니다만, Thread를 사용하면 동시에 처리하기 때문에 더 빠를 수 있습니다.(참고로 Thread의 인스턴스 생성 시간과 Thread 생성 시간이 있기 때문에 무조건 스레도로 병렬 처리 한다고 빨라지는 건 아닙니다.)

즉, 데이터 베이스나 꽤 많은 양의 데이터를 처리할 때, 이 쓰레드를 사용하면 좀 더 빨리 처리할 수 있습니다.


스레드(Thread)를 사용하면 메인 프로세스에서 독립된 스레드로 만들어져 실행되는 병렬 처리라는 것을 설명했습니다. 그런데 사양에 따라서 이 메인 프로세스에서 값을 기다려야 할 때가 있습니다.

class Node{
  // 멤버 면수
  private int data = 0;
  // 멤버 변수 값에 더하는 함수
  public void addData(int data) {
    this.data += data;
  }
  // 멤버 변수 값 반환 함수
  public int getData() {
    return this.data;
  }
}
public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // Node 인스턴스 생성
      final Node node = new Node();
      // 스레드 생성
      Thread th = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          // 
          node.addData(i);
        }
      });
      // 스레드 실행
      th.start();
      // 콘솔 출력
      System.out.println("Node - data = " + node.getData());
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 결과를 보면 Thread 안에서 0부터 99까지 더하는 식을 넣었지만 쓰레드 밖의 결과에서는 0가 나왔습니다.

이유는 병렬 처리가 되다 보니 콘솔 출력이 될 때 아직 스레드의 반복문이 실행이 실행이 되지 않았기 때문입니다.


그렇다면 스레드가 끝날 때까지 기다려야 하는데 이 때 사용하는 함수가 join입니다.

class Node{
  // 멤버 면수
  private int data = 0;
  // 멤버 변수 값에 더하는 함수
  public void addData(int data) {
    this.data += data;
  }
  // 멤버 변수 값 반환 함수
  public int getData() {
    return this.data;
  }
}
public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // Node 인스턴스 생성
      final Node node = new Node();
      // 스레드 생성
      Thread th = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          // 
          node.addData(i);
        }
      });
      // 스레드 실행
      th.start();
      // 스레드가 끝날 때까지 기다린다.
      th.join();
      // 콘솔 출력
      System.out.println("Node - data = " + node.getData());
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

join함수는 스레드를 호출하는 쪽에서 기다리는 함수입니다.


스레드 내부에서 정해진 시간 동안 기다리게 할 수도 있습니다.

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Example {
  // 실행 함수
  public static void main(String[] args) {
    try {
      // 날짜 포멧
      DateFormat format = new SimpleDateFormat("hh:MM:ss");
      Thread th = new Thread(() -> {
        try {
          // 0부터 99까지 반복문
          for (int i = 0; i < 100; i++) {
            // 현재 시간 콘솔 출력
            System.out.println(format.format(new Date()));
            // 쓰레드를 1초 동안 정지
            Thread.sleep(1000);
          }
        } catch (Throwable e) {
          e.printStackTrace();
        }
      });
      // 스레드 실행
      th.start();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

Thread.sleep는 스레드 내부에서 스레드를 잠시 멈추게 하는 함수입니다. 파라미터는 밀리 초 기준으로 1000이 1초입니다.

위에 현재 시간을 콘솔에 출력했는데 1초단위로 스레드를 멈추게 하니 1초마다 콘솔에 값이 출력되는 스레드가 되었습니다.


스레드는 프로세스를 병렬 처리가 가능하게 하는 리소스입니다. 그런데 이 병렬 처리가 마냥 프로그램의 성능을 향상 시키는 건 아닙니다.

일단 스레드를 생성하는데 속도가 많이 빠르지는 않습니다. 즉, 처리해야 할 데이터 양이 적거나 간단한 수식 계산이면 스레드를 생성하지 않고 하는 것이 오히려 성능이 더 좋을 수 있습니다.

또 스레드가 엄청 많아지면 결국에는 스레드도 하나의 리소스이기 때문에 프로그램 내부에서 리소스를 관리해야 합니다. 즉, 스레드가 너무 많아지면 성능이 떨어지는 경우가 많습니다.


여기까지 Java에서의 스레드(Thread)를 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.