Thread

박용민·2025년 10월 5일

스레드 안전성(Thread Safety)

여러 스레드가 동시에 같은 객체나 자원에 접근할 때, 예상치 못한 동작이나 데이터 손상이 발생하지 않도록 보장하는 성질

⚙️ 쓰레드 안정성이 깨지는 이유

  • 여러 스레드가 공유 변수에 접근하면서 동시에 수정할 떄
  • 작업 도중 다른 스레드가 중간 상태의 데이터를 읽을 떄
  • 캐시, 컴파일러 최적화, CPU 메모리등으로 가시성(visibility) 문제가 생길 떄

🎫 스레드 안정성을 보장하는 방법

  • 불변 객체 (Immutable Object) : 한 번 생성 후 변경되지 않기에 여러 스레드가 공유해도 안전함(String)
  • 동기화 (Synchronization : synchronized, ReentrantLock 등을 사용하여 한 번에 하나의 스레드만 접근하도록 제한
  • ThreadLocal : 스레드마다 독립적인 변수를 사용하도록 하여 공유를 피함
  • 원자 연산 (Atomic Operation) : AtomicInteger, AtomicReference 등 CAS 기반의 락-프리 방식

🔒 락(Lock)

여러 스레드가 동시에 공유 자원에 접근하지 못하도록 제어하는 메커니즘

암묵적인 락 (Implicit Lock)

synchronized 키워드를 사용할 때, JVM이 자동으로 제공하며 모든 객체(Object)는 자동으로 모니터 락을 사용 JVM이 해당 객체의 락을 자동으로 획득/해제

재진입성(Reentrancy)

어떤 메서드가 락을 걸고 있는데, 내부에서 같은 락을 사용하는 메서드를 다시 호출할 때
락이 중복으로 걸린다면 교착상태(deadlock) 발생할 수 있어 하나의 스레드가 같은 락을 여러 번 획득할 수 있는 성질

public class ReentrantExample {
    public synchronized void outer() {
        System.out.println("outer()");
        inner(); // 내부에서도 같은 락 사용
    }

    public synchronized void inner() {
        System.out.println("inner()");
    }
}

🔍가시성(visibility)

한 스레드가 어떤 값을 수정해도 그 값이 즉시 다른 스레드에게 보이지 않을 수 있는 현상
CPU 캐시와 레지스터를 이용할때 각 스레드는 자신만의 작업 메모리에 변수 값을 복사해서 사용
해당 오래된 데이터를 스테일 데이터(Stale Data)라고 한다.

volatile은 가시성 보장을 제공한 스레드가 즉시 메인 메모리에 반영되고, 다른 스레드가 다음에 읽을 때 메인 메모리에서 최신 값을 읽도록 강제 가시성만 보장하지, 원자성(atomicity) 은 보장하지 않음

private static volatile boolean running = true;

재배치

프로그램의 실행 순서가 코드에 작성된 순서와 다르게 CPU와 컴파일러에 의해 재배열 되는 현상
성능 최적화로 인해 컴파일러가 최적화를 통해 불필요한 명령 제거 및 순서 변경
병렬 실행을 통해 CPU 파이프라인 & 캐시 최적화, 자바에서는 JMM을 정의

// x = 0, y = 0 결과 가능성 존재
// why? 컴파일러나 CPU가 순서 변경할 수 도 있음
// 초기 값: x = 0, y = 0
int a = 0, b = 0;
int x = 0, y = 0;

// Thread 1
a = 1;        // (1)
x = b;        // (2)

// Thread 2
b = 1;        // (3)
y = a;        // (4)

JMM(Java Memory Mode)

  • as-if-serial 원칙 : 단일 스레드에서는 코드가 순서대로 실행된 것처럼 보여야 함
  • happens-before 관계 : 한 작업이 다른 작업보다 먼저 일어남을 보장하는 관계
  • 쓰기(write) 는 이전 코드보다 먼저 실행되며, 읽기(read) 는 이후 코드보다 늦게 실행되도록 강제

재배치 방지 방법

  • volatile을 추가해 재배치 방지
  • synchronized 블록, final 필드 초기화

🚀스레드 한정

하나의 객체(또는 데이터)를 오직 하나의 스레드에서만 접근하도록 설계 단계에서부터 제한하는 것

유형설명예시
지역 변수(Local variable confinement)메서드 내부 지역 변수는 기본적으로 해당 스레드에만 존재int sum = 0;
스택 한정(Stack confinement)지역 변수뿐 아니라 스택에 존재하는 객체 참조List<String> list = new ArrayList<>(); (메서드 안에서만 사용)
ThreadLocal confinement스레드별로 독립된 값을 보관ThreadLocal<T> 사용
프레임워크 한정특정 스레드 전용 환경(ex. Servlet 요청, Reactor pipeline 등)HttpServletRequest, Reactive Context

스택 한정(Stack confinement)

스레드의 호출 스택(Call Stack) 에 존재하는 변수(지역 변수)는 오직 그 스레드만 접근
스레드마다 완전히 독립된 변수이며 동기화가 전혀 필요 없음
객체 참조를 외부로 넘기지 않을 때만 유효

public List<String> unsafe() {
    List<String> list = new ArrayList<>();
    // ...
    return list; // ❌ 외부로 유출됨 — 더 이상 스택 한정 아님
}

ThreadLocal

각 스레드별로 독립적인 저장 공간을 제공, 스레드마다 같은 변수 이름으로 다른 값을 저장

private static final ThreadLocal<String> currentUser = new ThreadLocal<>();

Spring 트랜잭션 내부 구조에서의 ThreadLocal

Spring은 @Transactional을 사용할 때, 현재 트랜잭션의 상태(커넥션, 동기화 여부 등)를 ThreadLocal에 저장

  1. 트랜잭션 시작 시 @Transactional 메서드 진입
  2. DataSourceTransactionManager가 새로운 DB 커넥션을 열고, 등록
  3. 같은 스레드 안에서 Repository, Service 등이 실행될 때 현재 트랜잭션 커넥션을 재사용
  4. 트랜잭션이 끝나면 ThreadLocal에서 커넥션을 제거

스레드 불변성

멀티스레드 환경에서 여러 스레드가 동시에 같은 객체를 읽더라도, 객체의 상태가 절대 변하지 않기 때문에 안전(Thread-safe) 하다는 뜻 즉, 동기화(synchronization) 없이도 안전하게공유 가능

구분의미예시동기화 필요 여부
가변 객체 (mutable)상태가 변할 수 있음ArrayList, HashMap필요함 (synchronized, Lock)
불변 객체 (immutable)상태가 변하지 않음String, Integer, LocalDateTime필요 없음

불변 객체 특징

✅ 스레드 안전성 — 동기화가 필요 없고, 경쟁 상태(race condition)가 없음
✅ 단순성 — 동기화 코드가 사라져서 코드가 단순해짐
✅ 캐싱 및 공유 가능 — 같은 값을 여러 스레드에서 안전하게 재사용 가능
✅ 디버깅 쉬움 — 상태 변화가 없으므로 추적이 간단함
⚠️객체 생성 비용과 메모리 사용량이 늘어날 수 있음

불변 객체 조건

  • 객체 상태를 변경하는 매서드를 제공하지 않음
  • 모든 필드를 final로 선언
  • 필드는 생성자를 통해 한 번만 초기화
  • 객체를 확장(상속)하지 못하도록 final 클래스를 생성
  • 가변 객체를 참조한다면, 깊은 복사를 사용

안전공개

JMM에서는 스레드간 가시성이 기본적으로 보장되지 않기 때문에 객체가 한 스레드에서 생성되어 다른 스레드에 안전하게 공개되는 것을 보장하는 방법

안전 공개 보장하는 방법

  • 불변 객체 사용
  • 정적 초기화
  • final 필드
  • 동기화된 블록
  • volatile
  • 안전한 병렬 자료구조 사용

✈️동기화

멀티스레드 환경에서 여러 스레드가 동시에 공유 자원에 접근할 때, 일관성과 안전성을 보장하기 위한 메커니즘

동기화가 필요한 이유

스레드는 동시에 같은 메모리 공간을 공유

  • 힙(Heap): 모든 객체 인스턴스가 저장되는 영역. 모든 스레드가 공유
  • 스택(Stack): 각 스레드마다 별도. 로컬 변수와 호출 스택 프레임이 있음(스레드 로컬)
  • 레퍼런스(참조): 스택의 로컬 변수가 힙에 있는 객체를 가리키는데 해당 객체는 여러 스레드가 동시에 접근할 수 있음

동기화 미적용시 문제점

⚠️원자성(Atomicity) - count++은 단일 연산이 아닌 읽기 -> 증가 -> 쓰기의 복합 연산이다 때문에 중간에 다른 스레드가 끼어들면 문제가 발생
⚠️가시성(Visibility) - 한 스레드의 변경을 다른 스레드가 즉시 보지 못할 수 있음(캐시등)
⚠️명령 재정렬 - 컴파일러나 CPU가 최적화를 위해 실행 순서를 바꿀 수 있음

🍃 Spring에서 동기화

스프링은 자바의 동기화 개념 위에 추상화된 동시성 제어를 제공

Singleton Bean의 동기화 주의점

스프링의 기본 Bean 스코프는 싱글톤 즉, 하나의 인스턴스가 여러 스레드에서 동시에 사용

@Service
public class UserService {
    private int counter = 0;

    public void increment() {
        counter++; // 위험 ❌
    }
}

해결 방법
✅@Scope("prototype") 으로 Bean을 요청 단위로 분리
✅메서드 내부에 지역 변수만 사용 (Thread-safe)
✅synchronized 또는 Lock 사용
✅상태 없는(stateless) Bean 설계로 변경

@Transactional 동기화

트랜잭션 격리 수준(isolation level) 을 통해 DB 단위의 동시성 제어를 수행 DB에 대한 동시에 접근하는 트랜잭션 제어

@Scheduled, @Async 환경에서의 동기화

스프링은 비동기(@Async)나 스케줄(@Scheduled) 작업을 스레드 풀(ThreadPoolTaskExecutor) 기반으로 실행

Redis Lock / DB Lock / @Lock (JPA)

Spring에서는 로컬 락 대신 분산 환경에서의 분산 락을 자주 사용
▶️Redis 기반 락 -> Redisson, Lettuce
▶️DB 기반 락 → select ... for update
▶️JPA → @Lock(LockModeType.PESSIMISTIC_WRITE)

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select u from User u where u.id = :id")
User findByIdForUpdate(Long id);

🍂Spring에서 동기화 정리

구분설명동기화 주체
자바 synchronizedJVM 레벨 락객체/클래스
ReentrantLock고급 락 제어객체
volatile가시성 보장변수
@TransactionalDB 트랜잭션 단위DB
Spring Singleton Bean공유 객체로 주의 필요Bean 인스턴스
분산 락(Redis 등)멀티 서버 간 락외부 저장소

컬렉션

컬렉션은 기본적으로 스레드 안전 ❌

ArrayList, HashMap, HashSet 같은 컬렉션들(Legacy)은 기본적으로 동기화되어 있지 않음
ArrayIndexOutOfBoundsException이나 데이터 손상이 발생 할 수 있음

동기화된 컬렉션

한 번에 하나의 스레드만 컬렉션에 접근하도록 보장하는 컬렉션 래퍼(wrapper) 즉, 내부적으로 synchronized 블록으로 보호된 컬렉션

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());

주의점

반복(iteration) 할 때는 반드시 명시적 동기화 블록으로 감싸야 안전

synchronized (list) {
    for (int n : list) {
        System.out.println(n);
    }
}

병렬 컬렉션(Concurrent Collections)

JDK 1.5부터 등장한 java.util.concurrent 동시성(concurrency)을 고려해 설계된 더 효율적인 대안
▶️ConcurrentHashMap : 락 분할(Lock Striping)로 고성능 Map
▶️CopyOnWriteArrayList : 읽기 위주에 유리한 List :
▶️ConcurrentLinkedQueue : 비차단(Non-blocking) Queue
▶️ConcurrentSkipListMap, ConcurrentSkipListSet : 정렬 + 동시 접근 가능

컬렉션 정리

구분예시동기화 방식특징
LegacyVector, Hashtable, Stack메서드 전체 synchronized간단하지만 느림
WrapperCollections.synchronizedList(...)외부에서 synchronized wrapper 제공범용성 좋지만 iterator 주의
ConcurrentConcurrentHashMap, CopyOnWriteArrayList세밀한 락 or Lock-free성능 우수, 최신 방식

ConcurrentHashMap

고성능의 동시성(Map) 구현체로, 다수 스레드가 동시에 읽기/쓰기할 때 synchronized 전체 락 대신 세분화된 동기화(CAS + per-bin locking / lock-free 읽기) 로 높은 병렬성을 제공

▶️읽기 작업은 대부분 락 없이(lock-free) 수행되어 매우 빠름
▶️쓰기 작업은 CAS(비차단) + 해당 bin(버킷) 단위의 synchronized 로 처리
▶️원자적(key 단위) 연산: putIfAbsent, remove, replace, computeIfAbsent, compute, merge 등 전체 맵을 잠그지 않음
▶️ Node<K,V>[] table — 각각의 인덱스에 단일 연결 리스트(chain) 혹은 트리(트리화된 bin) 가 존재

BlockingQueue

생산자–소비자(Producer–Consumer) 패턴의 핵심이자, 스레드 간 안전하게 데이터를 교환하기 위한 큐 락이나 wait()/notify() 없이도 스레드 간 안전한 통신
▶️큐가 비어 있으면: 데이터를 꺼내려는 스레드는 대기
▶️큐가 가득 차 있으면: 데이터를 넣으려는 스레드는 대기

ThreadPoolExecutor

스레드풀(ThreadPoolExecutor) 내부에서도 BlockingQueue가 사용
▶️스레드가 바쁠 때는 큐에 작업을 put()
▶️큐가 꽉 차면 정책(예: RejectedExecutionHandler)에 따라 처리 거부

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100)
);

블로킹 메소드(Blocking Method)

메소드가 즉시 반환하지 않고, 특정 조건이 충족될 때까지 현재 스레드를 멈추는(대기하는) 메소드
=> 논블로킹(Non-blocking) 또는 비동기(Asynchronous) 로 대체하려는 추세

인터럽터블 메소드(Interruptible Method)

스레드가 블로킹된 상태일 때, 다른 스레드가 interrupt() 를 호출하면 즉시 깨어나 InterruptedException을 던지는 메소드

블로킹 메소드, 인터럽터블 메소드 정리

구분블로킹 메소드인터럽터블 메소드
정의조건이 충족될 때까지 스레드가 멈춤블로킹 중에도 interrupt() 호출 시 깨어남
대표 예시Thread.sleep(), InputStream.read(), BlockingQueue.take()Thread.sleep(), BlockingQueue.take(), Lock.lockInterruptibly()
인터럽트 반응 여부반응하지 않을 수도 있음반드시 InterruptedException 발생
사용 목적대기 상태로 CPU 낭비 방지대기 중 스레드 제어 가능하게 함
사용 위치I/O, 큐, 락, 리소스 대기스레드 종료 제어, 타임아웃 제어

동기화 클래스

상태 정보를 사용해 스레드 간의 작업 흐름을 조절할 수 있도록 만들어진 모든 클래스

래치(CountDownLatch)

하나 이상의 스레드가 어떤 작업이 완료될 때까지 기다리도록 하는 카운트 다운 기반 동기화 도구
카운트가 0이 되면 종료 및 다시 사용 불가, 단일/다중 스레드 동시 대기 가능

  1. 초기화 시 카운트(count) 지정
  2. 스레드가 await() 호출하면 카운트가 0이 될 때까지 블로킹
  3. 다른 스레드가 countDown() 호출할 때마다 카운트 감소
CountDownLatch latch = new CountDownLatch(3);

Runnable worker = () -> {
    System.out.println(Thread.currentThread().getName() + " 작업 완료");
    latch.countDown();
};

new Thread(worker).start();
new Thread(worker).start();
new Thread(worker).start();

latch.await(); // 3번 카운트 다운될 때까지 대기
System.out.println("모든 작업 완료");

FutureTask

비동기 작업의 결과를 표현하고, 나중에 결과를 가져올 수 있도록 하는 Future 구현체
한 번만 실행 가능하며 이미 완료된 작업은 get() 시 즉시 결과 반환

  1. Callable 또는 Runnable 기반으로 생성
  2. get() 호출 시 작업 완료 전이면 블로킹, 완료되면 결과 반환
Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();

System.out.println("결과: " + future.get()); // 작업 완료될 때까지 블로킹

Semaphore

동시에 허용 가능한 스레드 수를 제한하는 동시성 제어 도구
공유 자원 제한 (예: DB 커넥션 풀, 쓰레드 풀 등)

  1. 내부 카운트(permit) 유지
  2. acquire() → permit 감소, 0이면 대기
  3. release() → permit 증가, 대기 중 스레드 깨움
Semaphore semaphore = new Semaphore(5); // 최대 5 스레드 허용

Runnable task = () -> {
    try {
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + " 접근 허용");
        Thread.sleep(1000);
        semaphore.release();
    } catch (InterruptedException e) {}
};

for (int i = 0; i < 5; i++) new Thread(task).start();

CyclicBarrier

여러 스레드가 일정 지점까지 작업 완료를 기다린 후 동시에 다음 단계 진행
반복적 동기화 필요할 때 적합, 선택적으로 barrier action 지정 가능 (모든 스레드 도착 시 실행)

  1. 초기화 시 참여자 수(parties) 지정
  2. 모든 스레드가 await() 호출할 때까지 블로킹
CyclicBarrier barrier = new CyclicBarrier(5, () -> System.out.println("모두 도착! 다음 단계 시작"));

Runnable task = () -> {
    System.out.println(Thread.currentThread().getName() + " 준비 완료");
    barrier.await(); // 모든 스레드 대기
};

for (int i = 0; i < 5; i++) new Thread(task).start();
클래스블로킹 방식재사용 가능용도특징
CountDownLatchawait() 블로킹작업 완료 대기한 번만 사용 가능
FutureTaskget() 블로킹비동기 결과Callable/Runnable 기반
Semaphoreacquire() 블로킹동시 접근 제한permit 수로 동시성 제어
CyclicBarrierawait() 블로킹여러 스레드 동기화반복 단계 동기화 가능, barrier action

🪅Task 실행

순차적 실행

Main 스레드

모든 작업을 main 스레드에서 순차 실행

public class SequentialExample {
    public static void main(String[] args) {
        task("A");
        task("B");
        task("C");
    }

    static void task(String name) {
        System.out.println(name + " 작업 시작");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println(name + " 작업 완료");
    }
}

Thread.join()

join()으로 이전 스레드 종료 대기

public class JoinExample {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> task("A"));
        Thread t2 = new Thread(() -> task("B"));
        Thread t3 = new Thread(() -> task("C"));

        t1.start();
        t1.join(); // t1이 끝날 때까지 기다림

        t2.start();
        t2.join(); // t2가 끝날 때까지 기다림

        t3.start();
        t3.join(); // t3가 끝날 때까지 기다림
    }

    static void task(String name) {
        System.out.println(name + " 작업 시작: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println(name + " 작업 완료");
    }
}

ExecutorService(단일 스레드)

내부 큐로 순서 보장하며 실행

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

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
		
        // 작업 큐에 순서대로 쌓은 후 하나씩 실행
        executor.submit(() -> task("A"));
        executor.submit(() -> task("B"));
        executor.submit(() -> task("C"));

        executor.shutdown();
    }

    static void task(String name) {
        System.out.println(name + " 작업 시작: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println(name + " 작업 완료");
    }
}

⚠️단일 스레드 사용시 주의점

항목설명
① 긴 작업(Blocking)하나의 스레드가 모든 요청을 처리하므로, 한 작업이 오래 걸리면 전체가 멈춤
② CPU 점유율 불균형하나의 코어만 사용하므로 멀티코어 CPU 성능 활용 불가
③ UI/이벤트 블로킹(특히 GUI) 한 스레드에서 UI 갱신과 로직을 같이 처리하면 화면 멈춤 현상
④ 예외 발생 시 전체 종료예외가 잡히지 않으면 프로그램 전체가 중단됨
⑤ 테스트 중 착각단일 스레드에서는 잘 동작하지만, 멀티 환경에서 race condition이 숨겨질 수 있음

병렬적 실행

Thread 직접 사용

public class ParallelWithThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> task("A"));
        Thread t2 = new Thread(() -> task("B"));
        Thread t3 = new Thread(() -> task("C"));

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

    static void task(String name) {
        System.out.println(name + " 시작: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println(name + " 완료");
    }
}

ExecutorService

import java.util.concurrent.*;

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

        for (int i = 1; i <= 5; i++) {
            int id = i;
            executor.submit(() -> task(id));
        }

        executor.shutdown();
    }

    static void task(int id) {
        System.out.println("작업 " + id + " 실행 중: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println("작업 " + id + " 완료");
    }
}

병렬 스트림 (Parallel Stream)

import java.util.stream.IntStream;

public class ParallelStreamExample {
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 10)
                 .parallel()  // 병렬 모드로 전환
                 .forEach(i -> {
                     System.out.println("작업 " + i + " 실행 중: " + Thread.currentThread().getName());
                 });
    }
}

CompletableFuture

import java.util.concurrent.*;

public class ParallelCompletableFuture {
    public static void main(String[] args) {
        CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> task("A"));
        CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> task("B"));
        CompletableFuture<Void> f3 = CompletableFuture.runAsync(() -> task("C"));

        CompletableFuture.allOf(f1, f2, f3).join(); // 모두 끝날 때까지 대기
        System.out.println("모든 작업 완료!");
    }

    static void task(String name) {
        System.out.println(name + " 시작: " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println(name + " 완료");
    }
}

⚠️멀티 스레드 사용시 주의점

항목설명
① 공유 자원 접근 문제 (Race Condition)여러 스레드가 동시에 같은 변수를 수정하면 데이터가 꼬임
② 가시성(Visibility)한 스레드가 변경한 값이 다른 스레드에 즉시 보이지 않을 수 있음
③ 원자성(Atomicity)복합 연산(x = x + 1)은 중간 상태에서 끼어들 수 있음
④ 교착상태(Deadlock)여러 스레드가 서로 락을 기다리며 멈춤
⑤ 스레드 폭발(Thread Explosion)작업마다 스레드를 무한 생성 → 메모리 부족 / Context Switching 과다
⑥ 스케줄링 예측 불가스레드 실행 순서는 JVM과 OS 스케줄러에 따라 매번 다름
⑦ 예외 전파 문제스레드 내부에서 던진 예외는 main으로 전파되지 않음
⑧ 테스트 복잡도 증가타이밍 이슈, 동기화 문제로 재현 어려움

웹 요청시 싱글 스레드 vs 멀티 스레드

싱글 스레드 요청

blocking 방식

하나의 스레드가 요청을 순서대로 처리
▶️한 번에 하나의 요청만 처리
▶️요청 처리 중이면 다음 요청은 대기
▶️간단하지만 성능이 매우 낮음

public class SingleThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("단일 스레드 서버 시작");

        while (true) {
            Socket socket = serverSocket.accept(); // 요청이 올 때까지 대기 (Blocking)
            handleRequest(socket); // 요청 하나 처리 완료 후 다음 요청 처리
        }
    }

    private static void handleRequest(Socket socket) throws IOException {
        System.out.println("요청 처리 중: " + socket);
        try (socket) {
            socket.getOutputStream().write("200OK");
        }
    }
}

멀티 스레드 요청

요청마다 새로운 스레드를 생성

▶️요청마다 새 스레드가 생성
▶️동시에 여러 요청을 처리 가능
▶️메모리 부족 & 컨텍스트 스위칭 과다 발생

public class MultiThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("멀티 스레드 서버 시작");

        while (true) {
            Socket socket = serverSocket.accept(); // 요청 수신
            new Thread(() -> handleRequest(socket)).start(); // 요청마다 새 스레드 생성
        }
    }

    private static void handleRequest(Socket socket) {
        try (socket) {
            System.out.println("요청 처리 중: " + Thread.currentThread().getName());
            socket.getOutputStream().write("200OK");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
스레드 풀 사용

스레드 풀로 미리 일정 수의 스레드를 만듬

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class ThreadPoolWebServer {
    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(5); // 스레드 5개
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("스레드풀 서버 시작");

        while (true) {
            Socket socket = serverSocket.accept(); // 요청 수신
            executor.submit(() -> handleRequest(socket));
        }
    }

    private static void handleRequest(Socket socket) {
        try (socket) {
            System.out.println("요청 처리 중: " + Thread.currentThread().getName());
            socket.getOutputStream().write("200OK");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

스레드풀(Thread Pool)

스레드를 매번 새로 만들고 버리는 대신, 미리 만들어둔(또는 재활용 가능한) 스레드들을 모아놓은 풀(Pool)

ExecutorService pool = new ThreadPoolExecutor(
    5,                       // corePoolSize : 항상 유지되는 기본 스레드 개수
    10,                      // maximumPoolSize : 최대 허용 스레드 개수
    60L, TimeUnit.SECONDS,   // keepAliveTime : idle 스레드 유지 시간
    new LinkedBlockingQueue<>(100), // 작업 큐
    Executors.defaultThreadFactory(), 스레드 생성 방법 지정
    new ThreadPoolExecutor.AbortPolicy() // 큐 초과 시 예외
);

🎉Done

0개의 댓글