[Java] Java의 멀티스레드

Thread 클래스에서 직접 생성

java.lang.스레드 Runnable을 클래스의 매개변수로 사용하여 생성자를 호출하여 생성할 수 있습니다.

Runnable은 작업자 스레드에서 실행할 수 있는 코드를 포함하는 개체입니다.

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}
​

Runnable 구현 클래스를 만들거나 익명 구현 개체를 만들거나 (Java 8부터) 람다 식을 사용하여 매개 변수로 전달할 수 있습니다.

작업자 스레드는 생성될 때 실행되지 않습니다. 시작하다() 실행하려면 메서드를 호출해야 합니다.

Thread thread = new Thread(() -> {
  // 스레드 실행 코드
});
​
thread.start();   // 작업 실행

스레드 하위 클래스에서 생성

작업자 스레드가 수행할 작업은 스레드를 실행 가능하게 만들지 않고 스레드에서 서브클래싱할 수 있습니다.

이때 달리다() 메서드를 재정의하여 스레드가 실행하는 코드를 작성합니다.

Thread thread = new Thread() {
  public void run() {
    // 실행 코드
  }
};

상속된 클래스를 구현할 수도 있지만 위와 같이 익명의 자식을 사용할 수도 있습니다.

Thread thread = new Thread(new Runnalbe() {
  public void run() {
    // 실행 코드
  }
});

이 코드는 위의 “Thread 클래스에서 직접 생성된” 메서드이며 실행 결과는 동일하지만 Thread 클래스를 직접 사용하거나 상속하는 방법이 다릅니다.

스레드 이름

메인 스레드의 이름은 “main”이고 직접 생성된 스레드의 이름은 기본적으로 “thread-n”입니다.

자랑하고 싶다면 이름 삽입() 방법을 사용할 수 있습니다.

스레드의 정적 메소드 전류 스레드() 다음을 통해 현재 실행 중인 스레드에 대한 참조를 얻을 수도 있습니다.

스레드 우선순위

Java의 스레드 스케줄링은 우선 순위 방법과 라운드 로빈 방법을 사용합니다.

  • 라운드 로빈
    • 비선점형 FIFO 방식의 공정성 향상을 위한 선점형 도입
    • 일정 시간 동안 실행되고 일찍 종료됩니다.

라운드 로빈 방법은 JVM에서 관리하며 코드로 제어할 수 없습니다.

우선이다 우선 순위 설정() 1에서 10 사이의 정수를 선택할 수 있습니다. (숫자가 높을수록 우선순위가 높아집니다.)

스레드 클래스의 상수로 MAX_PRIORITY, NORM_PRIORITY, MIN_PRIORITY 등이 제공됩니다.

동기화 방법 및 동기화 블록

여러 스레드가 메모리를 공유할 때 경합 상태가 발생할 수 있습니다.

중요한 섹션을 지정하는 Java 동기화 키워드를 지정합니다.

크리티컬 섹션에 액세스하면 다른 스레드가 크리티컬 섹션 코드를 실행하지 못하도록 개체가 즉시 잠깁니다.

public synchronized void method() {
  // critical section
}

인스턴스 메서드와 정적 메서드가 모두 지원됩니다.

전체 메서드 대신 메서드의 일부를 동기화 블록으로 지정할 수도 있습니다. 이 경우 공통 개체를 지정할 수 있습니다.

public void method() {
  ...
  synchronized(공유객체) {
    // critical section
  }
  ...
}

공통 객체가 그 자체일 때 그만큼 삽입할 수 있습니다

스레드가 동기화 블록에 들어가면 해당 개체를 잠그고 동기화 블록을 실행합니다.

모든 동기화 블록이 실행될 때까지 다른 스레드는 동기화 방법이나 해당 동기화 블록을 실행할 수 없습니다.

스레드 상태

상황열거 상수(Thread.State)설명

객체 생성 새로운 Thread 객체가 생성되었고 start()가 아직 호출되지 않았습니다.
실행 대기 실행 가능 언제든지 실행 중 상태로 전환할 수 있는 상태
부서지다 기다리다 다른 스레드의 알림을 기다리는 중
TIMED_WAITING 일정시간 대기
막힘 사용할 객체가 잠금 해제될 때까지 대기 상태
완전한 실행 상태

일시중지 시간은 TIMED_WAITING입니다. 스레드.수면() 일정 시간을 기다리는 것과 같습니다.

BLOCKED는 자원이 동기화 블록에 액세스할 수 있을 때까지 대기 상태입니다.

스레드 상태 제어

스레드 클래스 방법

  • 잠()
    • 정적 sleep() 메서드는 지정된 시간 동안 실행 중인 스레드를 일시 중지하는 데 사용됩니다.
    • 지정된 시간 전에 방해하다() 가 호출되면 InterruptedException이 발생합니다.
  • 생산하다()
    • 스레드가 무의미한 작업을 수행할 것으로 예상되는 경우 우선 순위가 같거나 더 높은 스레드로 실행을 넘길 수 있습니다.
    • 스레드는 RUNNABLE 상태로 돌아갑니다.
  • 연결하다()
    • 스레드는 다른 스레드와 독립적으로 실행되지만 다른 스레드를 실행하기 전에 완료될 때까지 기다려야 하는 경우가 있습니다.
    • 실행 중인 스레드 A가 다른 스레드 B의 join() 함수를 호출하면 스레드 B가 종료될 때까지 WAITING 상태에 들어갑니다.
    // ThreadA
    threadB.start();
    threadB.join(); // threadB가 종료될 때까지 일시 정지

객체 클래스의 메서드

자신의 작업이 끝나면 쓰레드가 번갈아가며 실행되면 상대방의 쓰레드가 정지 상태에서 해제되고, 쓰레드 자체가 정지된 협업 프로세스가 구현될 수 있다.

공유 객체는 각 쓰레드가 해야 할 일을 동기화 방식으로 나누고 아래의 방식으로 협업을 구현합니다.

  • 기다리다()
    • 현재 스레드를 WAITING 상태로 만듭니다.
  • 알림()
    • wait() 에 의해 대기 중인 스레드 중 하나를 RUNNABLE로 만듭니다.
  • 모두에게 알림()
    • wait() 를 사용하여 대기 중인 모든 스레드를 RUNNABLE로 만듭니다.

이것은 생산자-소비자 문제를 해결합니다.

생산자-소비자 문제를 해결하기 위한 공유 객체의 예

public class ItemBox {
  private final Item() buffer;
  private int front;
  private int rear;
  
  public ItemBox(int size) {
    this.buffer = new Item(size + 1);
    this.front = 0;
    this.rear = 0;
  }
​
  public synchronized void put(Item item) throws InterruptedException {
    while ((rear + 1) % buffer.length == front) {
      wait();
    }
    buffer(front) = item;
    front = (front + 1) % buffer.length;
    notifyAll();
  }
​
  public synchronized Item take() throws InterruptedException {
    while (rear == front) {
      wait();
    }
    Item item = buffer(rear);
    rear = (rear + 1) % buffer.length;
    notifyAll();
    return item;
  }
}
​

데이터 구조를 공부한지 좀 되어서 순환 큐 부분이 맞는지는 잘 모르겠지만… 그렇게 될겁니다.

참고로 이러한 BlockingQueue 기능은 Java의 java.util.concurrent에서 지원합니다.

안전한 스레드 종료

스레드 즉시 종료 그만하다() 스레드가 사용하는 리소스가 안전하지 않은 상태로 남아 있기 때문에 이 메서드는 JDK 1.2부터 사용되지 않습니다.

따라서 스레드를 안전하게 즉시 종료하는 방법은 다음과 같습니다.

중지 플래그를 사용하는 방법

public class SimpleThread extends Thread {
  private boolean stop;
  
  public void run() {
    while (!stop) { ... }
    // 자원 반납
  }
}

지금 setStop(참) 로 안전하게 끝낼 수 있습니다.

interrupt() 메서드를 사용하는 방법

스레드가 중지된 경우 방해하다() 메서드 실행 시 InterruptedException이 발생하므로 예외 처리를 통해 스레드를 안전하게 종료할 수 있습니다.

단, 실행 중이거나 RUNNABLE 상태이면 예외가 발생하지 않지만 향후 일시 중단되면 예외가 발생합니다.

그러므로 중단된 () 인터럽트 통과 여부를 확인하여 실행 중에 중단할 수 있습니다.

데몬 스레드

데몬 스레드는 메인 스레드의 작업을 지원하는 보조 역할을 하는 스레드입니다.

따라서 메인 스레드가 종료되면 데몬 스레드가 강제로 종료됩니다.

setDaemon(참) 를 통해 데몬 스레드로 설정할 수 있습니다.

스레드 그룹

스레드 그룹은 관련 스레드를 그룹화하고 관리하는 데 사용됩니다.

System → Main 그룹은 JVM에 의해 생성됩니다.

  • GC를 담당하는 종료자 스레드를 포함한 일부 스레드는 시스템 그룹에 속합니다.

스레드는 스레드 그룹에 속해야 하며 명시적으로 포함되지 않는 한 스레드를 생성한 스레드와 동일한 스레드 그룹에 포함됩니다.

  • getThreadGroup() : 쓰레드 그룹을 반환
  • Thread.getAllStackTraces() : 맵 내에서 실행 중인 모든 스레드에 대한 정보 실행될
  • 스레드 그룹은 ThreadGroup 생성자를 사용하여 만들 수 있습니다.
    • 기본 상위 그룹은 현재 스레드가 속한 그룹이 됩니다.

스레드 그룹 스택 나누기

스레드 그룹 방해하다() 가 실행되면 인터럽트가 속한 모든 스레드에서 인터럽트가 실행됩니다.


스레드 풀

병렬 처리 작업이 증가함에 따라 스레드의 수가 폭발적으로 증가하고 이때 스레드 생성 및 스케줄링으로 인해 많은 CPU 작업이 필요합니다.

따라서 쓰레드 풀은 제한된 개수의 쓰레드를 미리 관리하고 태스크 큐에 들어오는 태스크를 하나씩 처리하는 방식이다.

java.util.concurrent에서는 Executor 클래스에서 정적 메서드를 쉽게 생성할 수 있도록 ExecutorService 인터페이스를 제공합니다.

  • 코어 스레드: 스레드 풀에서 사용하지 않는 스레드를 제거할 때 유지해야 하는 최소 스레드 수입니다.
  • 따라서 NewFixedThreadPool은 최대 nThread까지 스레드를 생성한 후 유지됩니다.
  • (기본값) 추가된 스레드가 60초 동안 아무 작업도 하지 않으면 스레드가 종료되고 풀에서 제거됩니다.
  • newCachedThreadPool()
  • : 초기 스레드 0개, 코어 스레드 0개, 최대 스레드 수 Integer.MAX_VALUE
  • newFixedThreadPool(int nThreads)
    ExecutorService executorService = Executors.newFixedThreadPool(
      Runtime.getRuntime().availableProcessors()
    );   // 가용 CPU 코어 수만큼 최대 스레드를 사용하는 스레드풀
  • : 초기 스레드 0, 코어 스레드 nThreads, 최대 스레드 nThreads

ThreadPoolExecutor 개체를 직접 만들어 원하는 스레드 풀을 만들 수 있습니다.

ExecutorService threadPool = new ThreadPoolExecutor(
  3,    // 코어 스레드 개수
  100,  // 최대 스레드 개수
  120L, // 제거할 스레드 유휴 시간
  TimeUnit.SECONDS,  // 유슈 시간 단위
  new SynchronousQueue<Runnable>() // 작업 큐
);

스레드 풀 종료

  • 끄다() : 남은 작업을 종료하고 스레드 풀을 종료합니다.
  • 셧다운나우() : 강제종료

작업은 java.lang.Runnable과 java.util.concurrent.Callable로 분류됩니다.

차이점은 호출 가능작업이 완료된 후 결과 T를 반환합니다.

스레드 풀에 작업 요청

스레드 풀에 대한 작업 요청은 작업 결과 반환 여부에 따라 두 가지 방법으로 나뉩니다.

  • execute void(실행 가능)
  • : 태스크 처리 중 예외가 발생하면 스레드를 종료하고 스레드 풀에서 제거합니다.
  • 제출하다
    • 미래 전송(실행 가능)
    • 미래 전송(실행 가능, V)
    • 선물 제출(호출 가능)

    : 예외가 발생하면 쓰레드를 종료하지 않고 재사용한다.

Runnable의 run 메소드는 void 타입인데 어떻게 제출 결과를 반환할 수 있을까요?

외부 변수는 매개 변수로 전달되며 외부 변수는 즉석에서 직접 수정됩니다.

콜러블과 마찬가지로 결과는 미래입니다. 받다() 관련이 있을 수 있습니다

차단 작업 완료 알림

미래 객체의 받다() 메서드가 작업을 완료할 때까지 기다린 후 결과를 받게 됩니다.

그러므로 완료까지 객체라고도 합니다.

미래의 경우에도 submit(Runnable) 반환 값 없이 get()이 작업을 완료할 때까지 차단할 수 있습니다.

작업 완료 순서 알림

CompletionService는 여러 작업의 결과가 순차적으로 필요하지 않을 때 사용할 수 있습니다.

CompletionService<V> completionService = new ExecutorCompletionService<V>(
  executorService
);
  • 여론조사() : 완료된 작업의 미래를 반환합니다. (없으면 바로 null 반환)
  • 가져가다() : 완료된 작업의 미래를 반환합니다(없으면 차단됨).
  • 제출하다() : 스레드 풀에 작업 요청

작업 완료에 대한 콜백 메서드 알림

차단 방식과 달리 콜백 방식은 결과를 기다리지 않고 다른 함수를 실행합니다.

콜백 메소드로 클래스를 직접 정의하거나 비동기 통신 인터페이스인 java.nio.channels.CompletionHandler를 사용할 수 있습니다.

In CompletionHandler V는 결과 값 유형이고 A는 첨부 유형입니다. 따라서 A가 필요하지 않으면 무효로 남겨둘 수 있습니다.

private CompletinoHandler<Integer, Void> callback = new CompletionHandler<Integer, Void>() {
  @Override
  public void completed(Integer result, Void attachment) {
    ...
  }
  
  @Override
  public void failed(Throwable ex, Void attachment) {
    ...
  }
};
​
executorService.submit(new Runnable(() -> {
  try {
    ...
    callback.completed(result, null);
  } catch(Exception e) {
    callback.failed(e, null);
  }
} ));

다중 스레드 환경에서 하나의 스레드가 실행되는 동안 예외가 발생하면 동일한 프로세스 내에서 리소스를 공유하는 다른 스레드에 영향을 줄 수 있습니다. 따라서 다중 스레드 환경에서는 예외 처리에 주의를 기울여야 합니다.


출처: 이것은 자바입니다