1. 소개

이 사용방법(예제)에서는 스레드를 시작하고 병렬 작업을 실행하는 다양한 방법을 살펴보겠습니다.

이는 특히 기본 스레드에서 실행할 수 없는 장기 또는 반복 작업을 처리하거나 작업 결과를 기다리는 동안 UI 상호 작용을 보류할 수 없는 경우에 매우 유용합니다.

스레드의 세부 사항에 대해 자세히 알아보려면 Java에서 스레드의 수명 주기 에 대한 사용방법(예제)를 읽어보십시오 .

2. 쓰레드 실행의 기본

Thread 프레임워크 를 사용하여 병렬 스레드에서 실행되는 일부 논리를 쉽게 작성할 수 있습니다 .

Thread 클래스를 확장하여 기본 예제를 살펴보겠습니다 .

public class NewThread extends Thread {
    public void run() {
        long startTime = System.currentTimeMillis();
        int i = 0;
        while (true) {
            System.out.println(this.getName() + ": New Thread is running..." + i++);
            try {
                //Wait for one sec so it doesn't print too fast
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ...
        }
    }
}

이제 스레드를 초기화하고 시작하는 두 번째 클래스를 작성합니다.

public class SingleThreadExample {
    public static void main(String[] args) {
        NewThread t = new NewThread();
        t.start();
    }
}

NEW 상태(시작되지 않은 상태) 의 스레드에서  start()  메서드를 호출해야 합니다  . 그렇지 않으면 Java에서 IllegalThreadStateException 예외 인스턴스가 발생합니다 . 

이제 여러 스레드를 시작해야 한다고 가정해 보겠습니다.

public class MultipleThreadsExample {
    public static void main(String[] args) {
        NewThread t1 = new NewThread();
        t1.setName("MyThread-1");
        NewThread t2 = new NewThread();
        t2.setName("MyThread-2");
        t1.start();
        t2.start();
    }
}

우리의 코드는 여전히 매우 단순하고 온라인에서 찾을 수 있는 예제와 매우 유사합니다.

물론 이것은 너무 많은 컨텍스트 전환이나 너무 많은 메모리 사용을 피하기 위해 올바른 방법으로 리소스를 관리하는 것이 매우 중요한 프로덕션 준비 코드와는 거리가 멀습니다.

따라서 프로덕션 준비를 위해 다음을 처리할 추가 상용구를 작성해야 합니다.

  • 새로운 스레드의 일관된 생성
  • 동시 라이브 스레드 수
  • 스레드 할당 해제: 누수를 방지하기 위해 데몬 스레드에 매우 중요합니다.

원한다면 이러한 모든 사례 시나리오에 대한 자체 코드를 작성할 수 있지만 휠을 다시 발명해야 하는 이유는 무엇입니까?

3. ExecutorService 프레임워크

ExecutorService는 스레드 풀 디자인 패턴(복제된 작업자 또는 작업자-작업자 모델이라고도 함)을 구현하고 위에서 언급한 스레드 관리를 처리하며 스레드 재사용성 및 작업 Queue과 같은 몇 가지 매우 유용한 기능을 추가합니다.

특히 스레드 재사용성은 매우 중요합니다. 대규모 응용 프로그램에서 많은 스레드 개체를 할당 및 할당 해제하면 상당한 메모리 관리 오버헤드가 발생합니다.

작업자 스레드를 사용하면 스레드 생성으로 인한 오버헤드가 최소화됩니다.

풀 구성을 쉽게 하기 위해 ExecutorService 에는 쉬운 생성자와 큐 유형, 최소 및 최대 스레드 수, 명명 규칙과 같은 일부 사용자 지정 옵션이 함께 제공됩니다.

ExecutorService 에 대한 자세한 내용은 Java ExecutorService 사용방법(예제)를 참조하세요  .

4. Executor로 태스크 시작하기

이 강력한 프레임워크 덕분에 스레드 시작에서 작업 제출로 사고 방식을 전환할 수 있습니다.

실행기에 비동기 작업을 제출하는 방법을 살펴보겠습니다.

ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
    new Task();
});

우리가 사용할 수 있는 두 가지 방법이 있습니다. 아무 것도 반환하지 않는 execute계산 결과를 캡슐화하는 Future 를 반환하는 submit 입니다.

Futures 에 대한 자세한 내용은 java.util.concurrent.Future 사용방법(예제)를 참조하세요  .

5. CompletableFutures 로 작업 시작

Future 개체 에서 최종 결과를 검색하기 위해  개체에서 사용할 수 있는 get 메서드를 사용할 수  있지만 이렇게 하면 계산이 끝날 때까지 부모 스레드가 차단됩니다.

또는 작업에 더 많은 논리를 추가하여 블록을 피할 수 있지만 코드의 복잡성을 증가시켜야 합니다.

Java 1.8은 CompletableFuture 라는 계산 결과로 더 나은 작업을 수행하기 위해 Future 구조 위에 새로운 프레임워크를 도입했습니다 .

CompletableFuture는 CompletableStage를  구현하여 콜백을 연결하고 결과가 준비된 후 작업을 실행하는 데 필요한 모든 연결 작업을 피할 수 있는 다양한 메서드를 추가합니다.

작업을 제출하는 구현은 훨씬 간단합니다.

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync는 우리가 비동기적으로 실행하려는 코드를 포함하는 Supplier를 받습니다 . 이 경우에는 lambda 매개변수입니다.

작업은 이제 암시적으로  ForkJoinPool.commonPool() 에 제출되거나 우리가 선호하는 Executor를 두 번째 매개변수로 지정할 수 있습니다 .

CompletableFuture 에 대한 자세한 내용은 CompletableFuture 사용방법(예제)를 참조하세요  .

6. 지연되거나 정기적인 작업 실행

복잡한 웹 애플리케이션으로 작업할 때 특정 시간에, 어쩌면 정기적으로 작업을 실행해야 할 수도 있습니다.

Java에는 지연되거나 반복되는 작업을 실행하는 데 도움이 되는 몇 가지 도구가 있습니다.

  • java.util.타이머
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1.  시간제 노동자

타이머는 백그라운드 스레드에서 향후 실행을 위해 작업을 예약하는 기능입니다.

작업은 일회성 실행 또는 정기적인 간격으로 반복 실행되도록 예약할 수 있습니다.

1초 지연 후 작업을 실행하려는 경우 코드가 어떻게 보이는지 봅시다.

TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("Task performed on: " + new Date() + "n" 
          + "Thread's name: " + Thread.currentThread().getName());
    }
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);

이제 반복 일정을 추가해 보겠습니다.

timer.scheduleAtFixedRate(repeatedTask, delay, period);

이번에는 지정된 지연 후에 작업이 실행되고 일정 시간이 지나면 반복됩니다.

자세한 내용은 Java 타이머 사용방법(예제)를 참조하십시오  .

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 에는 Timer 클래스  와 유사한 메서드가 있습니다 .

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture<Object> resultFuture
  = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

예제를 마치기 위해  반복 작업에 scheduleAtFixedRate()를 사용합니다.

ScheduledFuture<Object> resultFuture
 = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

위의 코드는 100밀리초의 초기 지연 후에 작업을 실행하고 그 이후에는 450밀리초마다 동일한 작업을 실행합니다.

프로세서가 다음 발생 이전에 작업 처리를 완료할 수 없는 경우 ScheduledExecutorService는  다음  작업을 시작하기 전에 현재 작업이 완료될 때까지 대기합니다.

이 대기 시간을 피하기 위해  scheduleWithFixedDelay()를 사용할 수 있습니다 . 이름에서 알 수 있듯이 작업 반복 사이에 고정 길이 지연을 보장합니다.

ScheduledExecutorService 에 대한 자세한 내용은 Java ExecutorService 사용방법(예제)를 참조하세요  .

6.3. 어떤 도구가 더 낫습니까?

위의 예제를 실행하면 계산 결과가 동일하게 보입니다.

그렇다면  올바른 도구를 어떻게 선택합니까 ?

프레임워크가 다양한 선택을 제공하는 경우 정보에 입각한 결정을 내리기 위해 기본 기술을 이해하는 것이 중요합니다.

후드 아래에서 조금 더 깊이 잠수해 봅시다.

타이머 :

  • 실시간 보장을 제공하지 않습니다. Object.wait(long)  메서드를 사용하여 작업을 예약합니다.
  • 단일 백그라운드 스레드가 있으므로 작업이 순차적으로 실행되고 장기 실행 작업은 다른 작업을 지연시킬 수 있습니다.
  • TimerTask 에서 발생한 런타임 예외는 사용 가능한 유일한 스레드를 종료하므로 타이머가 종료됩니다.

ScheduledThreadPoolExecutor :

  • 원하는 수의 스레드로 구성 가능
  • 사용 가능한 모든 CPU 코어를 활용할 수 있습니다.
  • 런타임 예외를 포착하고 원하는 경우 이를 처리할 수 있습니다( ThreadPoolExecutor 에서 afterExecute 메서드를 재정의하여 ).
  • 예외를 발생시킨 작업을 취소하고 다른 작업은 계속 실행되도록 합니다.
  • 시간대, 지연, 태양시 등을 추적하기 위해 OS 스케줄링 시스템에 의존합니다.
  • 제출된 모든 작업이 완료될 때까지 기다리는 것과 같이 여러 작업 간에 조정이 필요한 경우 협업 API를 제공합니다.
  • 스레드 수명 주기 관리를 위한 더 나은 API를 제공합니다.

이제 선택은 뻔하죠?

7. FutureScheduledFuture 의 차이점

코드 예제에서 ScheduledThreadPoolExecutor가  특정 유형의 Future : ScheduledFuture를 반환하는 것을 관찰할 수 있습니다 .

ScheduledFuture는  FutureDelayed 인터페이스를 모두 확장하므로  현재 작업과 관련된 나머지 지연을 반환하는 추가 메서드 getDelay를 상속합니다. 작업이 주기적인지 확인하는 메서드를 추가하는 RunnableScheduledFuture 로 확장됩니다 .

ScheduledThreadPoolExecutor는  내부 클래스 ScheduledFutureTask를 통해 이러한 모든 구성을 구현 하고 이를 사용하여 작업 수명 주기를 제어합니다.

8. 결론

이 사용방법(예제)에서는 스레드를 시작하고 작업을 병렬로 실행하는 데 사용할 수 있는 다양한 프레임워크를 실험했습니다.

그런 다음 Timer 와  ScheduledThreadPoolExecutor 의 차이점에 대해 자세히 살펴보았습니다 .

기사의 소스 코드는 GitHub에서 사용할 수 있습니다 .

res – REST with Spring (eBook) (everywhere)