1. 스레드 풀 핵심 개념
스레드 풀의 작동 원리와 생성 방식
- 원리: 스레드 자원을 통합적으로 관리하여 재사용하고, 반복적인 생성/소멸 비용을 줄이며 실행 흐름을 조정하고 감시한다.
- 생성 이유: 스레드 생명 주기 관리 비용 절감 및 시스템 응답 속도 향상; 동시 실행 스레드 수 제어로 자원 고갈 방지; 실행 상태 추적 및 예외 처리 용이화.
- 생성 방법:
ThreadPoolExecutor,ScheduledExecutorService,ForkJoinPool세 가지 주요 접근법이 있다.
ThreadPoolExecutor 사용 예제
import java.util.concurrent.*;
public class CustomThreadPoolExample {
private static final ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 5, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) {
pool.execute(() -> System.out.println("작업 수행 중: " + Thread.currentThread().getName()));
pool.shutdown();
}
}
ScheduledExecutorService 활용
import java.util.Date;
import java.util.concurrent.*;
public class ScheduledTaskExample {
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
public static void main(String[] args) {
System.out.println("지연 작업 등록: " + new Date());
scheduler.schedule(() -> System.out.println("지연 작업 실행: " + new Date()), 3, TimeUnit.SECONDS);
System.out.println("주기적 작업 등록: " + new Date());
scheduler.scheduleAtFixedRate(() -> {
System.out.println("주기적 작업 실행: " + new Date());
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
}, 2, 4, TimeUnit.SECONDS);
scheduler.schedule(() -> {
scheduler.shutdown();
System.out.println("스케줄러 종료");
}, 20, TimeUnit.SECONDS);
}
}
ForkJoinPool 적용 사례
import java.util.concurrent.*;
public class ForkJoinCalculation {
public static void main(String[] args) {
int processors = Runtime.getRuntime().availableProcessors();
ForkJoinPool workerPool = new ForkJoinPool(processors);
int[] data = new int[1000];
for (int i = 0; i < data.length; i++) data[i] = i + 1;
ArraySumTask task = new ArraySumTask(data, 0, data.length);
int total = workerPool.invoke(task);
System.out.println("합계 결과: " + total);
workerPool.shutdown();
}
static class ArraySumTask extends RecursiveTask<Integer> {
private static final int LIMIT = 100;
private final int[] values;
private final int begin;
private final int finish;
ArraySumTask(int[] arr, int start, int end) {
this.values = arr;
this.begin = start;
this.finish = end;
}
@Override
protected Integer compute() {
if (finish - begin <= LIMIT) {
int sum = 0;
for (int i = begin; i < finish; i++) sum += values[i];
return sum;
}
int middle = (begin + finish) / 2;
ArraySumTask left = new ArraySumTask(values, begin, middle);
ArraySumTask right = new ArraySumTask(values, middle, finish);
left.fork();
return right.compute() + left.join();
}
}
}
2. 스레드 풀 구성 요소 및 크기 설정
- 핵심 파라미터: corePoolSize, maximumPoolSize, keepAliveTime, workQueue, threadFactory, RejectedExecutionHandler 포함
- 작동 로직:
- 현재 스레드 수가 corePoolSize 미만일 경우 새 스레드 생성
- corePoolSize 이상일 경우 작업 큐에 저장
- 큐가 가득 찼다면 최대 스레드 수까지 추가 생성
- maximumPoolSize 초과 시 거부 정책 적용
- 크기 설정 전략:
- CPU 집약형: 코어 수 + 1 권장
- I/O 집약형: 코어 수 × 2 또는 (대기시간/CPU시간+1)×코어수 계산
- 혼합형: 각 유형별 별도 풀로 분리 처리
3. 스레드 생명 주기 및 보안
| 상태명 | 설명 | 진입 조건 |
|---|---|---|
| NEW | 객체 생성 후 start() 호출 전 | Thread t = new Thread() |
| RUNNABLE | 실행 가능 상태(CPU 대기 포함) | t.start() |
| BLOCKED | 락 확보 실패로 인한 대기 | synchronized 블록 진입 실패 |
| WAITING | 무한 대기 상태(notify 필요) | Object.wait(), Thread.join() |
| TIMED_WAITING | 시간 제한 대기(auto wakeup) | Thread.sleep(), wait(timeout) |
| TERMINATED | 작업 완료 혹은 예외 종료 | run() 종료, UncaughtException |
데드 프로세스 개념
자식 프로세스가 종료되었으나 부모 프로세스가 SIGCHLD 신호를 처리하지 않아 잔존하는 상태
스레드 안전 구현 기법
- 상호 배제 동기화
- synchronized 키워드: 자동 락 획득/해제
- ReentrantLock: 수동 락 관리, 타임아웃/인터럽트 지원
- 비차단 동기화
- CAS(Compare-And-Swap) 기반 Atomic 클래스 사용
- 비동기화 솔루션
- 스레드 캡슐화(Local Variables)
- 불변 객체(final 키워드)
- ThreadLocal 활용
// ReentrantLock 예시
import java.util.concurrent.locks.ReentrantLock;
class CounterWithLock {
private final ReentrantLock mutex = new ReentrantLock();
private int value = 0;
public void increase() {
mutex.lock();
try {
value++;
} finally {
mutex.unlock();
}
}
}
// Atomic 연산 예시
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
}
// ThreadLocal 활용 예제
class LocalStorage {
private static final ThreadLocal<Integer> storage = ThreadLocal.withInitial(() -> 0);
public static void update() {
storage.set(storage.get() + 1);
}
}
4. 키워드와 락 메커니즘
volatile 키워드 특징
- 핵심 기능: 가시성 보장, 명령어 재배열 방지
- 제한사항: 원자성 미보장(i++ 등의 복합 연산 문제)
// volatile 싱글턴 패턴
public class VolatileSingleton {
private static volatile VolatileSingleton instance;
private VolatileSingleton() {}
public static VolatileSingleton getInstance() {
if (instance == null) {
synchronized (VolatileSingleton.class) {
if (instance == null) {
instance = new VolatileSingleton();
}
}
}
return instance;
}
}
ThreadLocal 활용 및 메모리 누수 방지
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;
public class DateFormatterUtil {
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
private static final ExecutorService executor = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
try {
String result = formatDate(new Date());
System.out.printf("%d번 작업: %s (%s)%n", taskId, result, Thread.currentThread().getName());
} finally {
formatter.remove(); // 중요: 메모리 누수 방지를 위한 remove 호출
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
}
public static String formatDate(Date input) {
return formatter.get().format(input);
}
}
volatile vs synchronized 비교
| 비교 항목 | volatile | synchronized |
|---|---|---|
| 적용 범위 | 변수 한정 | 메서드/블록 전체 |
| 블로킹 여부 | 없음 | 있음(BLOCKED 상태 발생) |
| 원자성 보장 | 미보장 | 보장 |
| 성능 오버헤드 | 낮음 | 높음 |
| 적합한 상황 | 플래그, 단순 통신 | 복잡한 임계 영역 |
synchronized 락 범위와 교착 상태 방지
- 오브젝트 락: 인스턴스 단위(this) 락 적용
- 클래스 락: 정적 메서드 또는 Class 객체 락 적용
public class LockScopeExample {
public synchronized void instanceLock() {} // 인스턴스 락
public static synchronized void classLock() {} // 클래스 락
public void customLock() {
Object monitor = new Object();
synchronized(monitor) { /* 특정 객체 락 */ }
}
}
교착 상태 회피 전략:
- 고정된 순서로 락 획득
- 락 획득 시 타임아웃 설정(tryLock)
- 락 보유 시간 최소화
public class DeadlockPrevention {
private static final Object FIRST_LOCK = new Object();
private static final Object SECOND_LOCK = new Object();
public void orderedAccessFirst() {
synchronized(FIRST_LOCK) {
synchronized(SECOND_LOCK) {
// 작업 내용
}
}
}
public void orderedAccessSecond() {
synchronized(FIRST_LOCK) { // 동일한 순서 유지
synchronized(SECOND_LOCK) {
// 작업 내용
}
}
}
}