메인 콘텐츠로 건너뛰기
Deep Thought
← 목록으로
Backend

멀티쓰레딩으로 동시성 프로그래밍 구현하기

신중선-- views
multithreadingconcurrencyparallel-programmingthread-management

멀티쓰레딩이란?

멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드를 생성해 동시에 작업을 처리하는 프로그래밍 기법입니다. 멀티프로세싱과 달리 같은 메모리 공간을 공유하므로 더 가벼운 병렬 처리가 가능합니다.

각 쓰레드는 독립적인 실행 흐름을 가지면서도 코드, 데이터, 힙 메모리를 공유하므로 효율적인 자원 활용과 빠른 데이터 교환이 가능합니다. 특히 I/O 대기 시간이 긴 작업이나 CPU 집약적 작업을 분리하여 전체 응용 프로그램의 성능을 크게 향상시킬 수 있습니다.

핵심 개념

1. 쓰레드의 구조와 메모리 공유

각 쓰레드는 고유한 스택과 프로그램 카운터를 가지지만, 힙 메모리와 코드 영역은 같은 프로세스의 다른 쓰레드들과 공유합니다.

public class ThreadMemoryExample {
    private static int sharedCounter = 0; // 힙에 저장, 모든 쓰레드가 공유
    
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            int localVar = 100; // 스택에 저장, 쓰레드 고유
            for (int i = 0; i < 1000; i++) {
                sharedCounter++; // 공유 변수 접근
            }
        });
        
        Thread thread2 = new Thread(() -> {
            int localVar = 200; // 다른 쓰레드의 localVar와 독립
            for (int i = 0; i < 1000; i++) {
                sharedCounter++;
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

2. 동기화와 경쟁 상태 해결

공유 자원에 대한 동시 접근으로 발생하는 경쟁 상태를 해결하기 위해 동기화 메커니즘을 사용해야 합니다.

public class SynchronizedExample {
    private int counter = 0;
    private final Object lock = new Object();
    
    // synchronized 키워드 사용
    public synchronized void incrementWithSync() {
        counter++;
    }
    
    // 명시적 락 사용
    public void incrementWithLock() {
        synchronized (lock) {
            counter++;
        }
    }
    
    // 원자적 연산 사용
    private final AtomicInteger atomicCounter = new AtomicInteger(0);
    
    public void incrementAtomic() {
        atomicCounter.incrementAndGet();
    }
}

3. 쓰레드 풀을 통한 효율적 관리

직접 쓰레드를 생성하는 대신 쓰레드 풀을 사용하면 자원을 효율적으로 관리할 수 있습니다.

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 고정 크기 쓰레드 풀
        ExecutorService executor = Executors.newFixedThreadPool(4);
        
        // 작업 제출
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + 
                    " executed by " + Thread.currentThread().getName());
                // 시간이 걸리는 작업 시뮬레이션
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        executor.shutdown();
    }
}

4. 비동기 프로그래밍과의 결합

CompletableFuture를 활용해 멀티쓰레딩과 비동기 프로그래밍을 결합할 수 있습니다.

public class AsyncMultithreadingExample {
    public CompletableFuture<String> processDataAsync(String data) {
        return CompletableFuture
            .supplyAsync(() -> {
                // 데이터 전처리 (별도 쓰레드에서 실행)
                return preprocessData(data);
            })
            .thenApplyAsync(preprocessed -> {
                // 메인 처리 (다른 쓰레드에서 실행)
                return processMainLogic(preprocessed);
            })
            .thenApplyAsync(result -> {
                // 후처리 (또 다른 쓰레드에서 실행)
                return postProcess(result);
            });
    }
    
    private String preprocessData(String data) {
        // CPU 집약적 전처리 작업
        return data.toUpperCase();
    }
    
    private String processMainLogic(String data) {
        // 메인 비즈니스 로직
        return "Processed: " + data;
    }
    
    private String postProcess(String result) {
        // 결과 후처리
        return result + " [DONE]";
    }
}

정리

구분 장점 주의사항
성능 병렬 처리로 응답성 향상, CPU 멀티코어 활용 과도한 쓰레드 생성 시 오버헤드 증가
자원 공유 메모리 공유로 빠른 데이터 교환 경쟁 상태와 데드락 위험
컨텍스트 스위칭 프로세스 대비 가벼운 전환 비용 동기화 오버헤드 발생 가능
구현 복잡도 단일 프로세스 내 간단한 통신 디버깅과 테스트의 어려움
  • 핵심 원칙: 적절한 동기화로 데이터 무결성 보장
  • 성능 최적화: 쓰레드 풀 활용과 작업 단위 최적화
  • 안정성 확보: 데드락 방지와 예외 처리 철저히 구현

References