본문 바로가기
[Naver Cloud Camp 7] 교육 정리

네이버 클라우드 캠프 55일차 230712

by 우기37 2023. 7. 12.

#1 교육정리

1) Thread

 

 

 

#2 Thread

스레드 개념)

스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.

모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다.

또한, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(Multi-Threaded-Process)라고 합니다.

 

프로세스는?

프로세스(process)란 단순히 실행 중인 프로그램(program)이라고 할 수 있습니다.

즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말합니다. 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성됩니다.

 

다시말해, 동작하고 있는 프로그램입니다.

보통은 한 개의 프로세스는 한가지의 일을 하지만, 쓰레드를 이용하면 한 프로세스 내에서 두가지 또는 그 이상의 일을 동시에 할 수 있게 됩니다. 그렇기에 Muilti Thread로도 불립니다.

 

 

java에서 multi thread가 사용되는 이유)

 

  • 백그라운드/배치 작업의 더 빠른 처리: 여러 작업을 동시에 수행해야 하는 경우 멀티스레딩을 통해 서로 다른 작업을 병렬로 진행할 수 있습니다. 결과적으로 전체 처리 시간이 단축됩니다.
  • 최신 프로세서를 활용하려면: 대부분의 최신 시스템에는 여러 개의 프로세서가 있고 각 프로세서에는 여러 개의 코어가 있습니다. 다중 스레딩을 사용하면 서로 다른 프로세서에서 서로 다른 스레드를 실행할 수 있으므로 시스템 리소스를 보다 효율적으로 사용할 수 있습니다.
  • 응답 시간 단축: 사용자는 Java 애플리케이션이 빠를 것으로 기대합니다 . 요청에 필요한 처리를 더 작은 청크로 나누고 서로 다른 스레드가 처리를 병렬로 처리하도록 함으로써 응답 시간을 줄일 수 있습니다.
  • 동시에 여러 사용자에게 서비스 제공: Tomcat , JBoss, Oracle WebLogic  IBM WebSphere 와 같은 Java 애플리케이션 서버는 수천 명의 사용자를 병렬로 지원할 것으로 예상됩니다. 멀티스레딩이 이를 달성할 수 있는 유일한 방법입니다. 처리할 각 요청에 대해 애플리케이션 서버에서 하나의 Java 스레드를 생성합니다.

 

출처 : https://www.eginnovations.com/blog/java-threads/

 

Main Thread)

 main() 메서드를 실행하는 기본 실행 흐름에서 새로운 실행 흐름으로 분기하고 싶다면,
 Thread 클래스를 정의할 때 분기해서 실행할 코드를 담으면 된다.
 그러면 두 개의 실행 흐름이 서로 왔다 갔다 하면서 실행된다.

 ## 멀티태스킹(multi-tasking)
 - 한 개의 CPU가 여러 코드를 동시(?)에 실행하는 것.
 - 실제는 일정한 시간을 쪼개 이 코드와 저 코드를 왔다갔다 하면서 실행한다.
 - 그럼에도 불구하고 외부에서 봤을 때는 명령어가 동시에 실행되는 것 처럼 보인다.
 - 왜? CPU 속도 워낙 빠르기 때문이다.

 ## CPU의 실행 시간을 쪼개서 배분하는 정책 : CPU Scheculing 또는 프로세스 스케줄링
 - CPU의 실행 시간을 쪼개 코드를 실행하는 방법이다.


 1) Round-Robin 방식
 - Windows OS에서 사용하는 방식
 - CPU 실행 시간을 일정하게 쪼개서 각 프로세스에 분배하는 방식


 2) Priority 방식
 - Unix, Linux 에서 사용하는 방식
 - 우선 순위가 높은 프로세스에 더 많은 실행 시간을 배정하는 방식
 - 문제점:
   - 우선 순위가 낮은 프로그램인 경우 CPU 시간을 배정 받지 못하는 문제가 발생했다.
   - 그래서 몇 년이 지나도록 실행되지 않는 경우가 나타났다.
 - 해결책?
   - CPU 시간을 배정 받지 못할 때 마다 
     즉 다른 프로세스에 밀릴 대 마다 우선 순위를 높여서
     언젠가는 실행되게 만들었다.
   - 이런 방식을 "에이징(aging) 기법"이라 부른다.

 ## 멀티 태스킹을 구현하는 방법
 1) 멀티 프로세싱
 - 프로세스(실행 중인 프로그램)를 복제하여 분기한다.
 - 그리고 분기된 프로세스를 실행시켜서 작업을 동시에 진행하게 한다.


 - 장점:
   - 분기하기가 쉽다. fork() 호출.
   - 즉 구현(프로그래밍)하기가 쉽다.

 - 단점:
   - 프로세스를 그대로 복제하기 때문에 
     프로세스가 사용하는 메모리도 그대로 복제된다.
   - 메모리 낭비가 심하다.
   - 복제된 프로세스는 독립적이기 때문에 
     실행 종료할 때도 일일이 종료해야 한다.

 2) 멀티 스레딩
 - 특정 코드만 분리하여 실행한다.
 - 따라서 프로세스가 사용하는 메모리를 공유한다.


 - 장점:
   - 프로세스의 힙 메모리를 공유하기 때문에 메모리 낭비가 적다.
   - 모든 스레드는 프로세스에 종속되기 때문에 프로세스를 종료하면
     스레드도 자동 종료된다.
 - 단점:
   - 프로세스 복제 방식에 비해 코드 구현이 복잡하다.

 ## 컨텍스트 스위칭(context switching)
 - CPU의 실행 시간을 쪼개 이 코드 저 코드를 실행할 때 마다
   실행 위치 및 정보(context)를 저장하고 로딩하는 과정이 필요하다.
 - 이 과정을 '컨텍스트 스위칭'이라 부른다.

 ## 스레드(thread)
 - '실'이라는 뜻을 갖고 있다.
 - 한 실행 흐름을 가리킨다.
 - 하나의 실은 끊기지 않은 하나의 실행 흐름을 의미한다.

 ## 스레드 생성
 - 새 실을 만든다는 것이다.
 - 즉 새 실행 흐름을 시작하겠다는 의미다.
 - CPU는 스레드를 프로세스와 마찬가지로 동일한 자격을 부여하여
   스케줄링에 참여시킨다.
 - 즉 프로세스에 종속된 스레드라고 취급하여
   한 프로세스에 부여된 실행 시간을 다시 쪼개 스레드에 나눠주는 방식이 아니다.
 - 그냥 단독적인 프로세스처럼 동일한 실행 시간을 부여한다.
 

 

멀티 스레드 실행 코드)

모든 자바 어플리케이션은 Main Thread가 main() 메소드를 실행하면서 시작됩니다. 예외는 없습니다. 이러한 Main Thread 흐름 안에서 싱글 스레드가 아닌 멀티 스레드 어플리케이션은 필요에 따라 작업 쓰레드를 만들어 병렬로 코드를 실행할 수 있습니다. 싱글 스레드 같은 경우 메인 스레드가 종료되면 프로세스도 종료되지만, 멀티 스레드는 메인 스레드가 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않습니다.

 // 멀티 스레드 적용 후
package com.eomcs.concurrent.ex1;

public class Exam0120 {

  // CPU의 시간을 쪼개서 왔다갔다 하면서
  // 동시에 실행하고픈 코드가 있다면,
  // 다음과 같이 Thread를 상속 받아
  // run() 메서드에 그 코드를 두어라!
  //
  static class MyThread extends Thread {
    @Override
    public void run() {
      // 기존 실행 흐름과 분리하여 따로 실행시킬 코드를
      // 이 메서드에 둔다.
      for (int i = 0; i < 1000; i++) {
        System.out.println("==> " + i);
      }
    }
  }

  public static void main(String[] args) {
    // => 동시에 실행할 코드를 담고 있는 Thread 객체를 생성한다.
    // => 그리고 현재 실행과 분리하여 작업을 시작시킨다.
    // => JVM은 이 스레드에 들어 있는 코드와 다음에 진행하는 코드를
    // 왔다갔다 하면서 처리할 것이다.
    new MyThread().start();

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

}

 

결과)

아래아 같이 두 개의 출력 실행이 왔다갔다 하면서 실행이 됩니다.

 

 

스레드의 우선순위)

위에서 잠깐 소개했던 Priority에 대해서 조금 더 자세하게 살펴보겠습니다.

 

자바에서 각 스레드는 우선순위(priority)에 관한 자신만의 필드를 가지고 있습니다.

이러한 우선순위에 따라 특정 스레드가 더 많은 시간 동안 작업을 할 수 있도록 설정할 수 있습니다.

 

필드 설명
static int MAX_PRIORITY 스레드가 가질 수 있는 최대 우선순위를 명시함.
static int MIN_PRIORITY 스레드가 가질 수 있는 최소 우선순위를 명시함.
static int NORM_PRIORITY 스레드가 생성될 때 가지는 기본 우선순위를 명시함.

getPriority()와 setPriority() 메소드를 통해 스레드의 우선순위를 반환하거나 변경할 수 있습니다.

스레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이며, 숫자가 높을수록 우선순위 또한 높아집니다.

 

하지만 스레드의 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값일 뿐입니다.

우선순위가 10인 스레드가 우선순위가 1인 스레드보다 10배 더 빨리 수행되는 것이 아닙니다.

단지 우선순위가 10인 스레드는 우선순위가 1인 스레드보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받을 뿐입니다.

 

그리고 스레드의 우선순위는 해당 스레드를 생성한 스레드의 우선순위를 상속받게 됩니다.

 

예제코드)

class ThreadWithRunnable implements Runnable {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(Thread.currentThread().getName()); // 현재 실행 중인 스레드의 이름을 반환함.

            try {

                Thread.sleep(10);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

 

public class Thread02 {

    public static void main(String[] args){

        Thread thread1 = new Thread(new ThreadWithRunnable());

        Thread thread2 = new Thread(new ThreadWithRunnable());
        
①      thread2.setPriority(10); // Thread-1의 우선순위를 10으로 변경함.

②      thread1.start(); // Thread-0 실행

③      thread2.start(); // Thread-1 실행

 

        System.out.println(thread1.getPriority());

        System.out.println(thread2.getPriority());

    }

}

 

실행 결과)

5

10

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

Thread-1

Thread-0

 

main() 메소드를 실행하는 스레드의 우선순위는 언제나 5이므로, main() 메소드 내에서 생성된 스레드 Thread-0의 우선순위는 5로 설정되는 것을 확인할 수 있습니다.

 

위의 예제는 ②번 라인에서 Thread-0이 먼저 실행되고, ③번 라인에서 Thread-1이 나중에 실행됩니다.

 

따라서 만약 ①번 라인이 존재하지 않는다면, Thread-0이 먼저 실행되고, Thread-1이 나중에 실행될 것입니다.

 

하지만 ①번 라인에서 Thread-1의 우선순위를 10으로 변경했기 때문에, Thread-1이 나중에 실행됐더라도 우선순위가 Thread-0보다 높아 먼저 실행되는 것입니다.

 

예제 참조 : http://www.tcpschool.com/java/java_thread_concept

 

 

스레드 코드 예제)

 

스레드 만들기 I - Thread를 상속 받기)

Thread 클래스를 상속 받아 정의하고 run()을 재정의하여 별도로 분리해서 병행으로 실행할 코드를 두는 곳을 생성합니다.

그리고 thread의 서브 클래스는 인스턴스를 생성하여 start를 호출합니다.

// 스레드 만들기 I - Thread를 상속 받기
package com.eomcs.concurrent.ex3;

public class Exam0110 {

  public static void main(String[] args) {

    // 1) Thread 클래스를 상속 받아 정의하기
    // => 구현하기 편하다.
    // => 그런데 다중 상속이 불가능하기 때문에 다른 클래스를 상속 받을 수 없다.
    // => 즉 MyThread가 다른 클래스를 상속 받으면서 스레드가 될 순 없다.
    //
    class MyThread extends Thread {
      // 기존의 스레드에서 분리해서 새 스레드에서 실행하고픈 코드가 있다면,
      // run()을 재정의하여 그 메서드에 해당 코드를 두어라!
      @Override
      public void run() {
        // 별도로 분리해서 병행으로 실행할 코드를 두는 곳!
        for (int i = 0; i < 1000; i++) {
          System.out.println("===> " + i);
        }
      }
    }

    // 스레드 실행
    // => Thread의 서브 클래스는 그냥 인스턴스를 만들어 start()를 호출한다.
    MyThread t = new MyThread();
    t.start(); // 실행 흐름을 분리한 후 즉시 리턴한다. 비동기로 동작한다.

    // "main" 스레드는 MyThread와 상관없이 병행하여 실행한다.
    for (int i = 0; i < 1000; i++) {
      System.out.println(">>>> " + i);
    }
  }
}

// CPU 사용을 스레드에게 배분할 때, 스레드를 생성한 순서대로 배분하지는 않는다.
// OS의 CPU 스케줄링 정책에 따라 스레드가 실행된다.
// 즉 JVM에서 스레드를 실행하는 것이 아니라 OS가 실행한다.
// 결론!
// => 똑 같은 자바의 스레드 코드가 OS에 따라 실행 순서가 달라질 수 있다.
//
// 우선 순위로 조정하면 되지 않나요?
// => Windows OS의 경우 우선 순위(priority) 값이 실행 순서나 실행 회수에 큰 영향을 끼치지 않는다.
//    그래서 우선 순위의 값을 조정하여 스레드의 실행 회수를 조정하려 해서는 안된다.
// => 왜? OS에 따라 실행 정책이 다르기 때문이다.
// => 그냥 특정 코드를 동시에 실행하고 싶을 때 스레드를 사용한다고 생각하라!

 

스레드 만들기 II - Runnable 인터페이스 구현 + Thread)

// 스레드 만들기 II - Runnable 인터페이스 구현 + Thread
package com.eomcs.concurrent.ex3;

public class Exam0210 {

  public static void main(String[] args) {

    // 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(); // 실행 흐름을 분리한 후 즉시 리턴한다.

    // "main" 스레드는 Thread와 상관없이 병행하여 실행한다.
    for (int i = 0; i < 1000; i++) {
      System.out.println(">>>> " + i);
    }

  }

}

 

 

Runnable 인터페이스 구현 + Thread - 람다(lambda)로 구현하기)

// Runnable 인터페이스 구현 + Thread - 람다(lambda)로 구현하기
package com.eomcs.concurrent.ex3;

public class Exam0230 {

  public static void main(String[] args) {
    new Thread(() -> {
      for (int i = 0; i < 1000; i++) {
        System.out.println("===> " + i);
      }
    }).start();

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

  }

  static void m(Runnable obj) {

  }

}

 

 

스레드와 프로그램 종료)

// 스레드와 프로그램 종료
package com.eomcs.concurrent.ex3;

import java.util.Scanner;

public class Exam0310 {

  static class MyThread extends Thread {
    @Override
    public void run() {
      Scanner keyboard = new Scanner(System.in);
      System.out.print("입력하시오> ");
      String input = keyboard.nextLine();
      System.out.println("입력한 문자열 => " + input);
      keyboard.close();
    }
  }

  public static void main(String[] args) {

    // main 스레드에서 새 스레드 객체 생성하기
    // => 어떤 스레드에서 만든 스레드를 그 스레드의 자식 스레드라 부른다.
    // => 즉 다음 스레드는 main 스레드의 자식 스레드이다.
    // => 자식 스레드는 부모 스레드와 같은 우선 순위를 갖는다.
    MyThread t = new MyThread(); // 우선순위 5
    t.start();

    // 모든 스레드가 완료할 때까지 JVM은 종료되지 않는다.
    System.out.println("프로그램 종료?");
  }

}