아래의 내용은 java_grammer 레파지토리 C08Thread 디렉터리에 저장되어있는 내용을 정리함
- ThreadMain
- Library
일반적인 작업은 아니고 성능을 크게 올려야하는 작업 수행 필요 시 멀티스레드 작업을 해야할 일이 있음
스레드 기본 → 동시성 이슈 테스트 → 해결책 비교 → 실무 동기화 워크플로우
스레드(thread)란 프로세스 내에서 실제로 작업을 수행하는 주체
CPU 코어 1개당 1개의 Processor가 동작하고, 자바 프로그램은 main 스레드에서 시작됨
싱글 스레드 vs 멀티 스레드
모든 프로세스: 최소 1개 스레드 (main)
멀티스레드 프로세스: 2개 이상 → 비동기 동작 (실행 순서 보장 X)
멀티스레드 동작 원리
단순 계산은 오히려 싱글 스레드가 더 효율적 (문맥 교환 오버헤드 때문에)
cf) 무조건 멀티 스레드 프로그래밍이 좋나?
문객 교환을 위한 상태저장으로 소모되는 자원이 많으므로 무조건 좋은것은 아님
// C07ExceptionFileParsing.MyThread 참고
class MyThread extends Thread {
@Override
public void run() {
System.out.println("스레드 실행");
}
}
Thread t1 = new MyThread();
t1.start(); // start() → run() 자동 호출
Thread의 기본 run()은 빈 메서드라 상속 후 오버라이드 필수.
// 람다로 간단히
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 선호 이유: 코드의 낭비 없이 바로 익명객체를 만들 수 있음, 상속 제한 없음 (다른 클래스 상속 가능)
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
특정 메서드를 보호하며 프로그램 전체 동작 기준으로 멀티스레드로 동작하고있음
(프로그램 전체 기준 비동기가 맞으며, 동시성을 완전히 제한하지 않고 성능저하 발생 X)
public synchronized static void borrow() { // 한 번에 1개 스레드만
if (bookCount > 0) {
Thread.sleep(100);
bookCount -= 1;
System.out.println("대출완료");
}
}
결과: bookCount = 0 (정상)
동작: 메서드 진입시 락 획득 → 다른 스레드 대기 → 실행 완료 → 락 해제
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> Library.borrow());
t.start();
t.join(); // 이 스레드 완료까지 대기
}
결과: bookCount = 0 (정상)
단점: 사실상 단일 스레드처럼 동작(동기적) → 성능 저하
for (int i = 0; i < 1000; i++) {
Library.borrow(); // 순차 실행
}
결과: bookCount = 0 (정상)
StringBuffer buffer = new StringBuffer(); // 내부 메서드 synchronized
buffer.append("hello"); // 동시성 안전
StringBuilder builder = new StringBuilder(); // 일반 메서드
builder.append("hello"); // 동시성 이슈 발생 가능
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이 스레드 풀 관리 (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)으로 동시성 이슈 재현실무 실행 순서
Spring Boot 실행 → Tomcat 스레드 풀 자동 관리
→ Controller에서 @Transactional
→ Service에서 비즈니스 검증
→ Repository에서 Pessimistic/Optimistic Lock
→ Redis로 실시간 카운터
Thread를 생성하는 방식 1,2 이런것보다는 해결 예시를 더 집중적으로 보면 됨