[Naver Cloud Camp 7] 교육 정리

네이버 클라우드 캠프 58일차 230717

우기37 2023. 7. 17. 18:49

#1 교육정리

1) TreadPool 복습

 

 

 

#2 ThreadPool 복습

Executors & ExecutorService 사용)

먼저 사용자가 직접 Thread 관리하는데 어려움으로 인해 Java에서 제공하는 Executor 클래스와 ExecutorService 메서드를 통해 작업을 위임 합니다.

  • 자바로 스레드를 생성할 경우에는 아주 기본적으로는 아래와 같이 Thread 클래스와 Runnable 함수형 인터페이스를 구현해 Thread를 생성합니다.
  • 간단한 소스 같은 경우에는 쉽게 관리할 수 있지만 복잡해지는 경우에는 스레드를 사용자가 직접 관리하는 것은 매우 어렵습니다.
  • ex) 인터럽트 관리 
  • 이러한 관리의 어려운 문제를 해결하기 위해 스레드를 만들고 관리하는 작업을 위임을 하기 위해 Executors가 등장하게 됩니다.

java API를 살펴보면, Executor 클래스는 Object를 상속받으며, 아래와 같이 concurrent 패키지로 사용 가능합니다.

Executor, ExecutorService, ScheduledExecutorService 등의 메서드를 사용 가능합니다.

java.util.concurrent

Class Executors



  • public class Executors
    extends Object
    Factory and utility methods for Executor, ExecutorService, ScheduledExecutorService, ThreadFactory, and Callable classes defined in this package. This class supports the following kinds of methods:
    • Methods that create and return an ExecutorService set up with commonly useful configuration settings.
    • Methods that create and return a ScheduledExecutorService set up with commonly useful configuration settings.
    • Methods that create and return a "wrapped" ExecutorService, that disables reconfiguration by making implementation-specific methods inaccessible.
    • Methods that create and return a ThreadFactory that sets newly created threads to a known state.
    • Methods that create and return a Callable out of other closure-like forms, so they can be used in execution methods requiring Callable.

출처 : https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html

 

 

Executors의 특징)

  • Thread를 만들고 관리하는 것을 고수준의 API Executors에게 위임합니다.
  • Runnable만 개발자가 만들고 생성, 종료, 없애기 작업(일련의 작업)들은 Executors가 해줍니다.
  • 인터페이스는 크게 Executor와 ExecutorService가 있으나 실질적으로 ExecutorService를 사용합니다.
  • 개발자는 작업만 소스코드로 작성하면 됩니다.

Executors의 주요 역할)

  1. Thread 만들기: 애플리케이션이 사용할 Thread pool을 만들어 관리합니다.
  2. Thread 관리: Thread 생명 주기를 관리합니다.
  3. 작업 처리 및 실행: Thread로 실행할 작업을 제공할 수 있는 API를 제공합니다.

 

 

Executor / ExecutorService 사용법 원리 그림)

 

 

 

 

Executor 코드 예제)

다음은 코드와 함께 예제를 보며 살펴보겠습니다.

 

Executors 태스크 프레임워크 - 스레드풀 만들고 사용하기)

// Executors 태스크 프레임워크 - 스레드풀 만들고 사용하기
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0110 {

  public static void main(String[] args) {
    // 스레드풀을 생성한다.
    // - 최대 3개의 스레드를 생성한다.
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    // 스레드풀에 작업 수행을 요청한다.
    // - 작업은 Runnable 구현체로 작성하여 넘겨준다.
    // - 스레드풀은 스레드를 생성하여 작업을 수행시킨다.
    executorService
        .execute(() -> System.out.printf("%s - Hello!\n", Thread.currentThread().getName()));

    System.out.println("main() 종료!");
    // JVM은 main 스레드를 종료하더라도 나머지 스레드가 종료할 때까지 기다린다.
    // 스레드풀에서 생성한 스레드가 요청한 작업을 마치더라도
    // 다음 작업을 수행하기 위해 계속 실행된 채로 대기하고 있기 때문에
    // JVM은 종료하지 않는다.
  }
}

 

실행 결과)

아래 실행 결과와 같이 main() 스레드가 종료되었음에도 불구하고 JVM 은 종료되지 않습니다.(강제종료 해주어야 함!)

 

 

Executors 태스크 프레임워크 - 스레드풀 종료하기)

// Executors 태스크 프레임워크 - 스레드풀 종료하기
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0120 {

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    executorService.execute( 
        () -> {
          System.out.printf("%s - Hello!\n", Thread.currentThread().getName());

          try { 
            Thread.sleep(10000); // 현재 스레드를 10초 동안 멈춘다.
          } catch (Exception e) {}

          System.out.printf("%s 스레드 종료!\n", Thread.currentThread().getName());
        });

    // 스레드풀에 있는 모든 스레드들이 요청한 작업을 끝내면
    // 종료하도록 지시한다.
    // 모든 스레드가 종료될 때까지 기다리지 않고 바로 리턴한다.
    // shutdown() 호출 이후에는 새 작업 요청은 받지 않는다.
    // 즉 execute()를 호출하면 예외가 발생한다.
    executorService.shutdown();

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

10초 후에 모든 스레드가 종료됩니다.

execute()를 호출하면 예외가 발생되기에 try ~ catch 문으로 예외처리를 해주고, shutdown() 메서드를 호출하여 스레드를 종료해줍니다. shutdown() 메서드가 호출되고나서는 새 작업 요청은 받지 않습니다.

↓ (10초 후)

 

 

Executors 태스크 프레임워크 - 스레드풀 만들기 : 고정크기 스레드풀)

// Executors 태스크 프레임워크 - 스레드풀 만들기 : 고정크기 스레드풀
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0210 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("[%s] - 스레드에서 작업 실행 중...\n", Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("[%s] - 작업 종료 후 스레드 대기!\n", Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("[%s] 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    // 일단 스레드풀의 크기(3 개)만큼 작업 수행을 요청한다.
    // - 작업은 큐에 등록된 순서대로 보관된다.
    // - 스레드풀은 큐에서 작업을 꺼내 스레드에게 일을 시킨다.
    //
    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(9000));

    // 스레드풀의 크기를 초과해서 작업 수행을 요청한다면?
    // - 놀고 있는 스레드가 없을 경우,
    // 다른 스레드의 작업이 끝날 때까지 작업큐에 대기하고 있는다.
    // - 작업을 끝낸 스레드가 생기면 큐에서 작업을 꺼내 실행한다.
    //
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

스레드 작업 실행시는 OS에서 지정해준 순서대로 스레드를 실행합니다.

그 후에 2번 스레드가 3초 대기를 먼저하고, 다음 실행을 이어갑니다. 후에 1번 스레드가 실행 및 대기 후에 종료가 되고 다음 놀고 있는 3번 스레드가 종료되고 그 다음 작업을 끝낸 2번 스레드가 종료 후 대기하게 됩니다.

 

 

Executors 태스크 프레임워크 - 스레드풀 만들기 : 가변크기 스레드풀)

// Executors 태스크 프레임워크 - 스레드풀 만들기 : 가변크기 스레드풀
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0220 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드에서 작업 실행 중...\n", Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 작업 끝내고 스레드 대기!\n", Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) throws Exception {

    // 스레드의 수를 고정하지 않고 필요할 때마다 스레드를 생성하는 스레드풀이다.
    // 물론 작업을 끝낸 스레드는 다시 사용할 수 있도록 pool에 보관한다.
    ExecutorService executorService = Executors.newCachedThreadPool();

    // 놀고 있는 스레드가 없으면 새 스레드를 생성한다.
    //
    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(9000));
    executorService.execute(new MyRunnable(1000));

    // 작업을 끝낸 스레드가 생길 때까지 일부러 기다린다.
    //
    Thread.sleep(3000);

    // 그러면 새 스레드를 생성하지 않고
    // 작업을 끝낸 스레드가 요청한 작업을 처리한다.
    //
    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

 

Executors 태스크 프레임워크 - 스레드풀 만들기 : 한 개의 스레드를 갖는 스레드풀)

// Executors 태스크 프레임워크 - 스레드풀 만들기 : 한 개의 스레드를 갖는 스레드풀
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0230 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n", Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n", Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) throws Exception {

    // 한 개의 스레드만 갖는 스레드풀이다.
    ExecutorService executorService = Executors.newSingleThreadExecutor(); // nodejs 방식

    // 스레드가 한 개이기 때문에 순차적으로 실행한다.
    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(9000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

Executors의 newSingleThreadExecutor() 는 한 개의 스레드만 갖는 스레드풀 입니다.

 

 

Executors 태스크 프레임워크 - 작업 실행 : execute() )

// Executors 태스크 프레임워크 - 작업 실행 : execute()
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0310 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n",
            Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n",
            Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    // 스레드풀에 수행할 작업을 등록한다.
    // 스레드풀은 execute()를 호출한 순서대로 작업큐에 작업을 보관한다.
    // 그리고 놀고 있는 스레드가 있다면, 작업큐에서 작업을 꺼내 수행시킨다.
    // 놀고 있는 스레드가 없으면, 새로 스레드를 생성한다.
    // 스레드가 최대 개수라면 기존 스레드가 작업을 끝낼 때까지 기다린다.
    // => 수행한 작업의 종료 여부를 확인할 수 없다.
    executorService.execute(new MyRunnable(6000));

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

Executors 태스크 프레임워크 - 작업 실행 : submit() )

// Executors 태스크 프레임워크 - 작업 실행 : submit()
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Exam0320 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n",
            Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n",
            Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }
  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    // execute()와 같다.
    // => 단 작업의 종료 상태를 확인할 수 있는 Future 객체를 리턴한다.
    //
    Future<?> future1 = executorService.submit(new MyRunnable(2000));
    Future<?> future2 = executorService.submit(new MyRunnable(4000));

    // Future.get()
    // => 요청한 작업이 완료될 때 까지 기다린다.(pending)
    // => 요청한 작업이 완료되면 null을 리턴한다.
    //
    future2.get();
    System.out.println("두 번째 작업이 끝났음");

    future1.get();
    System.out.println("첫 번째 작업이 끝났음");

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

Executors 태스크 프레임워크 - 스레드풀 종료 : shutdown() )

// Executors 태스크 프레임워크 - 스레드풀 종료 : shutdown()
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0410 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("[%d] %s 스레드 실행 중...\n", this.millisec, Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("[%d] %s 스레드 종료!\n", this.millisec, Thread.currentThread().getName());

      } catch (Exception e) {
        System.out.printf("[%d] %s 스레드 실행 중 오류 발생!\n", this.millisec,
            Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));

    // => 더이상 작업 요청을 받지 말고
    // 이전에 요청한 작업(대기하고 있는 작업)들이 완료되면
    // 스레드를 종료하도록 예약한다.
    // => 작업 중인 스레드가 Not Runnable 상태가 아니라면
    // 작업이 끝날 때까지 기다린다.
    executorService.shutdown();

    // 작업 요청을 거절한다.
    // => 예외 발생!
    // executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

Executors 태스크 프레임워크 - 스레드풀 종료 : shutdownNow() )

// Executors 태스크 프레임워크 - 스레드풀 종료 : shutdownNow()
package com.eomcs.concurrent.ex7;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0420 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("[%d] %s 스레드 실행 중...\n", this.millisec, Thread.currentThread().getName());

        // Thread.sleep(millisec);
        for (long i = 0; i < 1_0000_0000; i++) {
          double r = Math.tan(3456.77889) / Math.random();

        }

        System.out.printf("[%d] %s 스레드 종료!\n", this.millisec, Thread.currentThread().getName());

      } catch (Exception e) {
        System.out.printf("[%d] %s 스레드 실행 중 오류 발생!\n", this.millisec,
            Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(1000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(7000));
    executorService.execute(new MyRunnable(8000));
    executorService.execute(new MyRunnable(9000));

    // 가능한 현재 수행 중인 작업들을 모두 멈추도록 지시한다.
    // => shutdown()과 차이점:
    // - 만약 Running 상태의 스레드가 Not Runnable 상태(sleep()/wait())에 놓인다면,
    // 바로 스레드를 멈출 기회라고 보고 스레드를 강제 종료 할 것이다.
    // - 즉 실행 중인 작업만 완료시키고 대기 중인 작업은 취소시키는 효과가 있다.
    // => Running 상태일 때에서 Not Runnable 상태가 때까지 기다린다.
    // => 그리고 취소한 대기 작업 목록을 리턴해준다.
    //
    List<Runnable> tasks = executorService.shutdownNow();
    System.out.println("실행 취소된 작업들:");
    System.out.println("--------------------------------");
    for (Runnable task : tasks) {
      System.out.println(((MyRunnable) task).millisec);
    }
    System.out.println("--------------------------------");

    // 물론 새 작업 요청도 거절한다.
    // => 예외 발생!
    // executorService.execute(new MyRunnable(4000));

    // shutdown() vs shutdownNow();
    // - shutdown()
    // 진행 중인 작업을 완료하고 대기 중인 작업도 완료한 다음 종료.
    // - shutdownNow()
    // 진행 중인 작업은 완료하고, 대기 중인 작업은 취소하고 그 목록은 리턴한다.
    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

Executors 태스크 프레임워크 - 스레드풀 종료 대기 : awaitTermination() )

// Executors 태스크 프레임워크 - 스레드풀 종료 대기 : awaitTermination()
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Exam0510 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n",
            Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n",
            Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }
  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(5000));

    executorService.shutdown();

    // 스레드풀의 모든 스레드가 종료되면 즉시 true를 리턴한다.
    // 만약 지정된 시간(예: 10초)이 경과할 때까지 종료되지 않았다면 false를 리턴한다.
    //
    if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
      System.out.println("아직 종료 안된 작업이 있다.");
    } else {
      System.out.println("모든 작업을 종료하였다.");
    }

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)

 

 

Executors 태스크 프레임워크 - 스레드풀 종료 대기 : awaitTermination() 활용)

// Executors 태스크 프레임워크 - 스레드풀 종료 대기 : awaitTermination() 활용
package com.eomcs.concurrent.ex7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Exam0520 {

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n", Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n", Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }
  }

  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(20000));

    // 실행 중인 작업 및 대기 중인 작업이 모두 끝나면 스레드풀을 종료하라!!
    executorService.shutdown();

    // 스레드풀의 모든 스레드가 종료될 때까지 기다린다.
    if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
      System.out.println("아직 종료 안된 작업이 있다.");
      System.out.println("남아 있는 작업의 강제 종료를 시도하겠다.");

      // => 만약 10초가 경과될 때까지 종료되지 않으면,
      // 대기 중인 작업은 강제 종료하라고 지시한다.

      // 강제 종료?
      // => 일단 대기 중인 작업은 모두 취소한다.
      // => 실행 중인 스레드 중 Not Runnable 상태에 있는 스레드는 강제 종료한다.
      // => 그 외 running 상태의 스레드는 강제 종료할 수 없다.
      // 예) 입.출력 대기 상태는 running 상태이다. Not Runnable 상태가 아니다.
      executorService.shutdownNow();

      // 그리고 다시 작업이 종료될 때까지 기다린다.
      if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        System.out.println("스레드풀의 강제 종료를 완료하지 못했다.");
      } else {
        System.out.println("모든 작업을 강제 종료했다.");
      }

    }

    System.out.println("main() 종료!");
  }
}

 

 

실행 결과)