[Java] 심화 - 스레드(thread)

이지연·2025년 12월 25일

개요

아래의 내용은 java_grammer 레파지토리 C08Thread 디렉터리에 저장되어있는 내용을 정리함

  • ThreadMain
  • Library
    일반적인 작업은 아니고 성능을 크게 올려야하는 작업 수행 필요 시 멀티스레드 작업을 해야할 일이 있음
    스레드 기본 → 동시성 이슈 테스트 → 해결책 비교 → 실무 동기화 워크플로우

스레드 기본 개념

스레드(thread)란 프로세스 내에서 실제로 작업을 수행하는 주체
CPU 코어 1개당 1개의 Processor가 동작하고, 자바 프로그램은 main 스레드에서 시작됨

싱글 스레드 vs 멀티 스레드

모든 프로세스: 최소 1개 스레드 (main)
멀티스레드 프로세스: 2개 이상 → 비동기 동작 (실행 순서 보장 X)

멀티스레드 동작 원리

  • CPU 코어가 정해진 시간마다 스레드 교체 (문맥 교환)
  • 스레드1(작업A) → 상태 저장 → 스레드2(작업B) → 상태 복원 → 스레드1
  • 동시에 1개 코어만 사용하므로 "동시 실행처럼 보이는 것"

단순 계산은 오히려 싱글 스레드가 더 효율적 (문맥 교환 오버헤드 때문에)

cf) 무조건 멀티 스레드 프로그래밍이 좋나?
문객 교환을 위한 상태저장으로 소모되는 자원이 많으므로 무조건 좋은것은 아님


스레드 생성 방법

방법(1) Thread 클래스 상속

// C07ExceptionFileParsing.MyThread 참고
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("스레드 실행");
    }
}

Thread t1 = new MyThread();
t1.start(); // start() → run() 자동 호출

Thread의 기본 run()은 빈 메서드라 상속 후 오버라이드 필수.

방법(2) Runnable 구현 (실무 표준)

// 람다로 간단히
new Thread(() -> System.out.println("스레드 실행1")).start();
new Thread(() -> System.out.println("스레드 실행2")).start();
new Thread(() -> System.out.println("스레드 실행3")).start();
new Thread(() -> System.out.println("스레드 실행4")).start();
new Thread(() -> System.out.println("스레드 실행5")).start();
System.out.println("hello") // main메서드의 단일스레드

실행 결과 (순서 랜덤)

스레드 실행1
스레드 실행5
스레드 실행4  
스레드 실행3
hello  // main 스레드
스레드 실행2

Runnable 선호 이유: 코드의 낭비 없이 바로 익명객체를 만들 수 있음, 상속 제한 없음 (다른 클래스 상속 가능)


동시성 이슈 실습 - Race Condition

Library 도서 대출 시뮬레이션 (책 100권)

개선 전 코드 (문제 발생)

public static void borrow() {
    if (bookCount > 0) {           // 모든 스레드 통과
        Thread.sleep(100);         // 대출 시간 시뮬
        bookCount -= 1;            // 동시에 실행 → bookCount 음수
    }
}

1000개 스레드 동시 실행

for (int i = 0; i < 1000; i++) {
    new Thread(Library::borrow).start();
}
Thread.sleep(2000);
System.out.println(Library.getBookCount()); // -891 (오류!)

문제 원인: 여러 스레드가 bookCount를 동시에 수정 → race condition

해결책 3가지 비교 테스트

1) synchronized 메서드 (권장)

특정 메서드를 보호하며 프로그램 전체 동작 기준으로 멀티스레드로 동작하고있음
(프로그램 전체 기준 비동기가 맞으며, 동시성을 완전히 제한하지 않고 성능저하 발생 X)

public synchronized static void borrow() {  // 한 번에 1개 스레드만
    if (bookCount > 0) {
        Thread.sleep(100);
        bookCount -= 1;
        System.out.println("대출완료");
    }
}

결과: bookCount = 0 (정상)
동작: 메서드 진입시 락 획득 → 다른 스레드 대기 → 실행 완료 → 락 해제

2) join() 사용 (비추)

for (int i = 0; i < 1000; i++) {
    Thread t = new Thread(() -> Library.borrow());
    t.start();
    t.join();  // 이 스레드 완료까지 대기
}

결과: bookCount = 0 (정상)
단점: 사실상 단일 스레드처럼 동작(동기적) → 성능 저하

3) 단일 스레드 (참고)

for (int i = 0; i < 1000; i++) {
    Library.borrow();  // 순차 실행
}

결과: bookCount = 0 (정상)


동기화 도구

StringBuffer vs StringBuilder

StringBuffer buffer = new StringBuffer();     // 내부 메서드 synchronized
buffer.append("hello");                      // 동시성 안전

StringBuilder builder = new StringBuilder(); // 일반 메서드
builder.append("hello");                     // 동시성 이슈 발생 가능

ConcurrentHashMap vs HashMap

ConcurrentHashMap<String,String> safe = new ConcurrentHashMap<>();
safe.put("java", "자바");  // putVal에 synchronized 포함

HashMap<String,String> unsafe = new HashMap<>();
unsafe.put("java", "자바"); // 동시성 이슈

실무 팁: static 변수는 Redis/DB로 관리. 알고리즘 문제에서만 StringBuilder 주의.


Spring/DB에서의 동시성 관리

웹 서비스 현실: Spring이 스레드 풀 관리 (Tomcat 스레드)

쇼핑몰 재고 이슈 예시

재고 1개 상태에서 100명 동시 구매 요청
→ 재고 -99 발생

해결 기술들

기술동작 원리사용 사례
Pessimistic Lock데이터 읽을 때 락 걸기재고 차감, 예약
Optimistic Lock업데이트시 변경 체크게시글 수정
Read Committed커밋된 데이터만 읽기기본 트랜잭션 격리
Repeatable Read조회 중 변경 불가MySQL 기본 수준

비관적 락 예시

SELECT * FROM stock WHERE id=1 FOR UPDATE;  -- 락 걸고 조회
UPDATE stock SET quantity = quantity - 1;

낙관적 락 예시

@Entity
class Stock {
    @Version  // 버전 체크
    private Long version;
}

워크플로우

1. 스레드 생성 (Runnable 람다)
2. 공유 자원 접근시 synchronized 확인
3. 실무: Spring 스레드 풀 + DB 락
4. Redis로 카운터 관리 (가장 이상적)

정리

  • synchronized로 race condition 이해
  • Thread.sleep(100)으로 동시성 이슈 재현
  • Runnable > Thread 상속

실무 실행 순서

Spring Boot 실행 → Tomcat 스레드 풀 자동 관리
→ Controller에서 @Transactional
→ Service에서 비즈니스 검증
→ Repository에서 Pessimistic/Optimistic Lock
→ Redis로 실시간 카운터
profile
Eazy하게

1개의 댓글

comment-user-thumbnail
2025년 12월 26일

Thread를 생성하는 방식 1,2 이런것보다는 해결 예시를 더 집중적으로 보면 됨

답글 달기