Java 다중 스레드 심화: 스레드 생명주기부터 동시성 안전까지

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 신호를 처리하지 않아 잔존하는 상태

스레드 안전 구현 기법

  1. 상호 배제 동기화
    • synchronized 키워드: 자동 락 획득/해제
    • ReentrantLock: 수동 락 관리, 타임아웃/인터럽트 지원
  2. 비차단 동기화
    • CAS(Compare-And-Swap) 기반 Atomic 클래스 사용
  3. 비동기화 솔루션
    • 스레드 캡슐화(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) { /* 특정 객체 락 */ }
    }
}

교착 상태 회피 전략:

  1. 고정된 순서로 락 획득
  2. 락 획득 시 타임아웃 설정(tryLock)
  3. 락 보유 시간 최소화
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) {
                // 작업 내용
            }
        }
    }
}

태그: java Multithreading threadpool concurrency synchronization

5월 29일 01:16에 게시됨