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;
}
}
}