Java 스레드 스케줄링 알고리즘의 핵심 이해

Java 스레드 상태와 전환

스레드 스케줄링을 이해하기 위해서는 먼저 스레드의 생명 주기와 상태 전환을 알아야 합니다. Java에서 스레드는 생성부터 종료까지 여러 가지 상태를 거치며, 각 상태는 특정 조건에 따라 전환됩니다.

  • NEW: 스레드 객체가 생성되고 아직 시작되지 않은 상태
  • Runnable: 실행 대기 또는 실행 중 상태
  • Blocked: 다른 스레드가 필요한 자원을 점유하고 있을 때 대기 상태
  • Waiting: 다른 스레드가 특정 조건을 만족할 때까지 무한정 대기
  • Timed Waiting: 일정 시간 동안 대기
  • Terminated: 실행을 완료하거나 예외 발생으로 종료된 상태

스레드 스케줄링의 두 가지 주요 방식

Java는 JVM 내부에서 스레드를 관리하고 실행 순서를 결정하는 스케줄링 메커니즘을 제공합니다. 이는 두 가지 주요 방식에 기반합니다.

1. 선점형 스케줄링 (Preemptive Scheduling)

우선순위가 높은 스레드가 CPU를 선점하는 방식입니다. Java에서는 스레드 우선순위를 1~10 사이로 설정할 수 있으며, 기본값은 5입니다.


Thread threadA = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("우선순위 높은 스레드 실행 중");
    }
});
threadA.setPriority(Thread.MAX_PRIORITY);
threadA.start();
  

하지만 운영체제마다 우선순위의 반영 정도가 다르기 때문에, 반드시 높은 우선순위 스레드가 먼저 실행된다고 보장할 수는 없습니다.

2. 라운드 로빈 (Time-Slice Scheduling)

모든 스레드에게 동일한 시간 할당량(CPU 시간 조각)을 부여하고 순환하면서 실행하는 방식입니다. 이 방식은 모든 스레드가 공정하게 처리될 수 있도록 보장합니다.


public class RoundRobinTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("스레드 1 실행");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("스레드 2 실행");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }
        });

        t1.start();
        t2.start();
    }
}
  

실행 결과는 두 스레드가 번갈아가며 실행되는 양상을 보일 수 있습니다.

고성능 멀티스레드 환경에서의 최적화 기법

고병렬 환경에서는 단순한 스레드 생성보다는 효율적인 자원 관리와 동기화 전략이 중요합니다.

1. CAS (Compare And Swap) 알고리즘

경쟁 조건을 최소화하기 위해 사용되는 무잠금(Non-blocking) 알고리즘입니다. AtomicInteger 같은 클래스 내부에서 사용됩니다.


import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger value = new AtomicInteger(0);

    public void increment() {
        while (!value.compareAndSet(value.get(), value.get() + 1)) {
            // 실패 시 재시도
        }
    }
}
  

2. 스레드 풀 관리

과도한 스레드 생성을 방지하기 위해 스레드 풀을 사용하는 것이 일반적입니다. ThreadPoolExecutor를 활용한 예시:


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

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

        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.execute(() -> {
                System.out.println("작업 " + taskNumber + " 실행 중");
            });
        }

        executor.shutdown();
    }
}
  

3. 잠금 세분화 (Lock Granularity)

동기화 범위를 최소화하여 대기 시간을 줄이는 방법입니다. 예를 들어, 전체 메서드를 잠그는 대신 필요한 데이터만 잠글 수 있습니다.


public class FineGrainedLock {
    private int counter = 0;
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void updateCounter() {
        synchronized (lockA) {
            counter++;
        }
    }

    public int getCounter() {
        synchronized (lockB) {
            return counter;
        }
    }
}
  

태그: java Thread concurrency Scheduling CAS

10월 11일 13:47에 게시됨