Java

[Effective Java] 아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

jwKim96 2023. 4. 4. 02:06

0. 들어가며

이 아이템은 스레드 스케줄러에게 의존적인 코드를 작성하지 말라는 내용입니다.

아래에서 조금 더 자세히 알아보겠습니다.

1. 스레드 스케줄링

스레드 스케줄링은 간단히 말해서, 스레드를 어떤 기준에 따라 순서대로 실행시키는행위 입니다.

기본적으로는 운영체제가 이런 작업을 모두 처리하는데요.

Java의 경우 JVM이 이를 대신 처리합니다.

1.1 JVM의 스레드 스케줄링

JVM의 스레드는 User-level-thread(ULT) 이고 실제 실행시에는 Kernal-level-thread(KLT)와 매핑됩니다.

  • User-level-thread : 사용자(프로그램)이 구현한 Thread
  • Kernel-level-thread : 운영체제(Kernel)에 구현된 Thread

하지만 KLT는 한정적이기 때문에 ULT를 스케줄링해야하는것이죠

  • JVM에 따라 구현이 다를 수 있다고 하는데 hotspot jvm docs은 OSThread를 JavaThread와 매핑함

이를 간단히 그림으로 나타내보면 다음과 같다고 할 수 있겠죠.

1.2 스레드 스케줄링에 영향받지 않는 코드

책에서는 “정확성이나 성능이 스레드 스케줄링에 따라 달라지면 다른 플랫폼에 이식하기 어렵다”고 합니다.

그래서 스레드 스케줄링에 영향을 받지 않기 위한 여러 기준을 제시합니다.

  • 프로세스 수를 고려하여 스레드 개수를 유지할 것
    • 스레드 풀 크기를 적절하게 설정할 것
  • 각 작업을 짧게 유지할 것
    • 스레드들의 대기시간을 최소로 만들기 위해
    • 하지만, 너무 짧으면 오히려 컨텍스트 스위치 오버헤드로 인해 더 느려질 수 있음
  • Busy waiting 상태가 되지 않도록 하기
    • 무한 루프에서 특정 상태를 기다리는 행위 등
    • 당장 처리할 작업이 없다면 실행하지 않기
    • 스레드를 낭비하는 행위를 지양

1.3 JVM 스레드 스케줄러로 보내는 메세지

Java의 Thread API 중 yield 라는 메서드가 있습니다.

public class ThreadYield {
    public static void main(String[] args) {
        Runnable r = () -> {
            int counter = 0;
            while (counter < 2) {
                System.out.println(Thread.currentThread()
                    .getName());
                counter++;
                Thread.yield();
            }
        };
        new Thread(r).start();
        new Thread(r).start();
    }
}

JVM의 Thread Scheduler에게 현재 스레드의 우선 순위를 낮춰도 된다는 힌트를 보내는 메서드입니다.
Thread Scheduler는 우선순위, 도착시간 이라는 두 가지 기준을 통해 스케줄링 알고리즘을 구성합니다.
이때 현재 스레드 작업의 중요도가 낮다면 우선순위를 낮춰서 다른 작업에게 넘겨주고자 할때 yield를 사용합니다.

하지만 말했지만 명령이 아니라 ‘힌트’이기 때문에, 무조건 반영된다는 보장이 없다고 합니다.
그래서 기존에 잘 동작하는 프로그램 성능 향상을 위해 ‘드물게’ 사용될 수는 있습니다만,
왠만한 동시성 성능 문제를 해결해야하는 상황에서는 yield는 적절한 해결방법이 아니라고 합니다.

결론 : 동시성 성능 문제 해결을 위해서 Thread.yield()는 적절한 해결 방법이 아니다.