J-C-02 Sleep 메서드와 동기화의 함정
글 정보
- 카테고리
- Programming/Java/Core
- 태그
- JavaLevel1
Thread.sleep의 역할
Thread.sleep(ms)는 스레드를 잠깐 멈추는 가장 기초적인 메서드입니다.
- 기본 동작
- 호출한 스레드를 TIMED_WAITING 상태로 보냅니다.
- 시간이 지나면 다시 RUNNABLE로 돌아옵니다.
- 즉, “멈췄다가 시간이 되면 실행 가능한 상태로 복귀”입니다.
- 핵심 포인트
sleep()은 “언제 다시 CPU를 받을지”를 보장하지 않습니다.- “최소한 그 시간만큼은 안 깨어난다”에 가깝습니다.
- “그 시간이 지나면 즉시 실행”은 아닙니다.
Sleep이 정확하지 않은 이유
sleep()은 설정한 시간보다 더 오래 쉬는 경우가 흔합니다.
- 이유
- OS 스케줄러가 CPU를 누구에게 줄지 결정합니다.
- 컨텍스트 스위칭 비용이 발생합니다.
- 시스템 부하가 높으면 재스케줄링이 늦어집니다.
- 결론
- 정밀한 타이밍이 필요한 로직에는 부적합합니다.
- “딱 1000ms 뒤에 실행” 같은 요구에는 맞지 않습니다.
우연에 맡기는 코드 Programming by Coincidence
sleep()로 순서를 맞추려는 코드는 불안정합니다.
- 대표 패턴
- “대충 1초 쉬면 저쪽 작업 끝났겠지”라는 가정입니다.
- 내 PC에서는 되는데 운영 서버에서는 깨질 수 있습니다.
- 스레드 스케줄링은 환경마다 결과가 달라집니다.
- 왜 위험한가
- 성공과 실패가 실행 타이밍에 의해 결정됩니다.
- 버그가 재현이 어렵습니다.
- 테스트가 통과해도 실제에서 터질 수 있습니다.
폴링 Polling 방식의 문제
루프를 돌면서 sleep()하고 상태를 확인하는 방식도 흔한 함정입니다.
- 전형적인 형태
while(!done) { Thread.sleep(10); }같은 코드입니다.- 문제점
- 불필요하게 CPU를 깨웠다 재우는 작업이 반복됩니다.
- 이벤트가 발생해도 다음 체크 타이밍까지 반응이 늦습니다.
- 지연과 낭비를 동시에 만들기 쉽습니다.
- 대안 방향
- “상태를 계속 확인”하지 말고 “신호를 기다리는 구조”로 바꿉니다.
- 즉, wait notify, join, 락과 조건, 동시성 유틸로 해결합니다.
올바른 종료는 interrupt로 설계합니다
sleep()보다 중요한 건 “멈추는 방법”이 아니라 “안전하게 깨우고 종료시키는 방법”입니다.
- interrupt가 하는 일
sleep()중인 스레드를interrupt()로 깨울 수 있습니다.- 깨워지면서 InterruptedException이 발생합니다.
- 이 예외를 잡는 지점이 “종료 신호 처리 지점”이 됩니다.
- 왜 flag보다 나은가
boolean flag는 스레드가 잠들어 있으면 즉시 반응하지 못합니다.interrupt()는 잠든 상태를 즉시 깨우는 트리거가 됩니다.- 종료를 “폴링”이 아니라 “이벤트”로 처리할 수 있습니다.
- 권장 패턴
InterruptedException을 잡으면- 작업 종료 루틴으로 빠지거나
- 인터럽트 상태를 복원하고(
Thread.currentThread().interrupt()) - 상위 로직에 종료 의도를 전달합니다.
스레드의 속성과 생명주기
스레드 주요 속성
스레드는 실행 단위이며 다음 속성을 가집니다.
- 식별자
- ID
- Name
- 우선순위
- Priority
- 상태
- NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
- 관계
- ThreadGroup
우선순위와 스케줄링의 현실
자바는 우선순위를 제공하지만, 절대적인 실행 순서를 보장하지 않습니다.
- 자바 우선순위 범위
MIN_PRIORITY(1)NORM_PRIORITY(5)MAX_PRIORITY(10)- 주의
- 실제 스케줄링은 JVM이 아니라 OS가 결정합니다.
- JVM은 힌트를 줄 뿐이고 OS 정책이 최종입니다.
- “우선순위 높이니까 먼저 실행”은 위험한 기대입니다.
스레드 상태 State 정리
스레드는 생성부터 종료까지 상태를 옮겨 다닙니다.
- NEW
- 객체만 만들어진 상태입니다.
start()가 호출되지 않았습니다.- RUNNABLE
- 실행 중이거나 실행 가능한 상태입니다.
- OS 스케줄러가 CPU를 줄 수 있는 후보입니다.
- “항상 CPU를 받고 있다”는 뜻은 아닙니다.
- TERMINATED
run()이 끝나서 종료된 상태입니다.- BLOCKED
- 락을 얻으려고 대기하는 상태입니다.
synchronized진입 시 락이 이미 점유 중이면 발생합니다.- 신호가 아니라 “락이 풀리는 순간”이 핵심입니다.
- WAITING
- 신호를 기다리는 상태입니다.
wait(),join()등에서 진입합니다.notify()나 대상 스레드 종료 같은 이벤트가 필요합니다.- TIMED_WAITING
- 시간을 정해두고 기다립니다.
sleep(ms),wait(ms),join(ms)가 대표입니다.- BLOCKED vs WAITING 차이
- BLOCKED는 락을 기다림입니다.
- WAITING은 신호를 기다림입니다.
- 멈추는 이유가 다르기 때문에 튜닝 포인트도 달라집니다.
멀티스레드가 필요한 이유
멀티스레드는 “CPU를 더 쓰기 위해서”만이 아니라 “멈추지 않는 UX”를 위해 필요합니다.
단일 스레드의 문제
I/O 작업은 느립니다.
- 느린 작업 예
- 파일 읽기
- 네트워크 통신
- DB 요청
- UI에서의 문제
- UI 스레드가 I/O를 하면 이벤트 루프가 멈춥니다.
- 화면이 굳는 프리징이 발생합니다.
해결은 스레드 분리입니다
역할을 나눕니다.
- UI 스레드
- 입력 처리
- 화면 갱신
- 이벤트 루프 유지
- 작업 스레드
- 오래 걸리는 I/O 처리
- 백그라운드 작업 수행
- 효과
- UX가 유지됩니다.
- 사용자 입장에서는 “앱이 멈추지 않음”이 가장 큰 가치입니다.
데몬 스레드 Daemon Thread
데몬 스레드는 “프로그램이 살아있는 동안만 의미가 있는 보조 작업”에 적합합니다.
- 특징
- 메인 스레드가 종료되면 데몬도 함께 종료됩니다.
- JVM이 데몬 스레드를 기다려주지 않습니다.
- 즉, 작업이 남아 있어도 강제 종료될 수 있습니다.
- 사용법
- 반드시
start()전에setDaemon(true)를 호출합니다. - 시작 이후에는 변경할 수 없습니다.
- 예시
- 가비지 컬렉터
- 로그 정리
- 주기적 자동 저장(데이터 손실 허용 여부를 먼저 검토해야 합니다)
Join 메서드와 JVM 스레드 모델
join은 확실한 순서 제어입니다
순서를 “추측”하지 말고 “기다림으로 보장”합니다.
- 동작
threadA.join()을 호출하면- 호출한 스레드는
threadA가 끝날 때까지 기다립니다. - 의미
- 비동기 작업의 완료 시점을 명확히 다룹니다.
- “끝났을 것 같다”가 아니라 “끝날 때까지 기다린다”입니다.
- 주의
- 대상 스레드가 끝나지 않으면 무한 대기합니다.
InterruptedException처리가 필요합니다.- 필요하면
join(timeout)을 사용합니다.
JVM과 OS 스레드 관계를 알아야 하는 이유
자바는 편하지만, 성능을 보려면 매핑 구조를 알아야 합니다.
- 추상화
- 자바 코드는 사용자 영역에서 스레드를 다룹니다.
- 실제 스케줄링은 커널 영역에서 이뤄집니다.
- 흔한 구조
- 최신 JVM은 보통 자바 스레드와 OS 스레드를 1대1로 매핑