1. UncaughtExceptionHandler
- 자바 쓰레드의 run() 메서드는 예외를 던질 수 없기 때문에 예외가 발생할 경우 run() 메서드 내에서만 예외를 처리해야 함
- RuntimeException이 발생하더라도 쓰레드 밖에서 예외를 잡고 처리하는 방법 없음
- 자바에서는 쓰레드가 비정상적으로 종료되었거나 특정한 예외를 쓰레드 외부에서 처리할 수 있도록 UncaughtExceptionHandler 인터페이스 제공
- 해당 인터페이스를 통해 어떤 원인으로 인해 쓰레드가 종료되었는지 대상 쓰레드와 예외를 파악 가능
코드 부연 설명
- 정적 메서드인 setDefaultUncaughtExceptionHandler() 메서드를 통해 모든 쓰레드에서 발생하는 uncaughtException에 대해 디폴트로 처리하는 인터페이스 설정 가능
- setUncaughtExceptionHandler() 메서드를 통해 특정 쓰레드에서 발생하는 uncaughtException을 처리하는 인터페이스 설정 가능
- setUncaughtExceptionHandler() 메서드가 setDefaultUncaughtExceptionHandler()보다 우선순위가 높음
- 쓰레드 2에서 예외가 발생헀을 때 sendNotificationToSlack() 메서드가 호출된 이유
2. 쓰레드를 중지하는 방법
- 자바 1 버전까지는 실행 중인 쓰레드를 임의로 중지하거나 종료할 수 있는 suspend(), stop() API 제공
- 하지만 해당 API들을 호출할 경우 쓰레드 종료 후 처리해야 하는 후처리 작업을 수행할 수 없기 때문에 두 API 모두 자바 1.2부터 Deprecated 처리
- suspend(), stop() API를 호출하는 대신 flag 변수 혹은 interrupt() 메서드를 활용해 쓰레드를 중지할 수 있음
2.1 flag 변수를 통해 쓰레드를 중지하는 방법
- flag 변수의 값이 어떤 조건에 만족할 경우 쓰레드의 실행을 중지하는 방식
- 동시성 문제를 고려하여 flag 변수를 atomic 변수 혹은 volatile 키워드를 사용하는 것을 권장
2.1.1 정상적으로 중지하는 코드
코드 부연 설명
- Thread2가 flag 변수인 running 상태 값을 false로 설정하고 해당 변수는 volatile 키워드로 선언했으므로 메모리에 상태가 업데이트됨
- Thread 1 또한 메모리 값에 올라온 running을 참고하기 때문에 while문을 탈출하며 쓰레드 종료
2.1.2 동시성 이슈가 발생하는 코드
코드 부연 설명
- Thread2가 flag 변수인 running 상태 값을 false로 설정했지만 해당 값은 캐시 메모리에만 업데이트됨
- 반면, Thread 1은 메모리 값에 올라온 running을 참고하기 때문에 while문을 탈출할 수 없어 자원이 고갈될 때까지 쓰레드 종료 안됨
2.2 interrupt() 메서드를 활용해 쓰레드를 중지하는 방법
Java 쓰레드 기본 API인 interrupt()를 복습하면 다음과 같습니다.
- interrupt() 메서드를 통해 특정 쓰레드에게 interrupt 신호를 보낼 수 있으며 해당 쓰레드의 실행 중단, 작업 취소, 강제 종료 등을 목적으로 사용
- 쓰레드는 인터럽트 발생 여부를 확인할 수 있는 상태 값인 interrupted를 가지고 있으며 디폴트 값은 false
- interrupted()와 isInterrupted() 메서드 모두 interrupted 상태값을 확인하는 용도
- interrupted()는 상태 값을 확인한 후 interrupted 상태를 false로 초기화시킴
- isInterrupted()는 상태 값을 확인한 후 별도 처리 안 함 (상태값 변경 X)
- sleep(), join() 등으로 인해 실행 중인 쓰레드가 Waiting 상태로 전환되었을 때 interrupt 신호를 받으면 InterruptedException이 발생하며 이때 interrupted 값이 false로 초기화됨
본론으로 돌아와 자바는 아래 코드처럼 interrupt() 메서드를 통해 쓰레드를 중지할 수 있습니다.
코드 부연 설명
- stopper 쓰레드가 worker 쓰레드에 interrupt 신호를 보냄
- 아래에 열거할 두 가지 케이스가 발생할 수 있으며 두 케이스 모두 쓰레드를 중지시킴
- worker 쓰레드가 Thread.sleep(500); 코드에 의해 대기 상태로 전환되었고 이 때 interrupt 신호를 받았다면 InterruptedException이 발생하여 쓰레드 중지
- worker 쓰레드가 sleep() 메서드로 인해 전환된 Waiting 상태에서 Runnable 상태로 전환되는 찰나에 interrupt 신호를 받을 경우 `!Thread.currentThread().isInterrupted()` 값이 false가 되어 while문 탈출하고 쓰레드 중지
3. 사용자 쓰레드 vs 데몬 쓰레드
자바 쓰레드는 다음과 같이 크게 두 가지 유형의 쓰레드로 구분 가능합니다.
- 사용자 쓰레드
- 데몬 쓰레드
자바 어플리케이션이 실행되면 JVM은 사용자 쓰레드인 메인 쓰레드와 나머지 데몬 쓰레드를 동시에 생성하고 시작합니다.
3.1 메인 쓰레드
- 어플리케이션에서 가장 중요한 부분으로서 어플리케이션을 실행할 때마다 메인 쓰레드가 생성되어 실행
- 메인 쓰레드는 어플리케이션을 실행하는 최초의 쓰레드이자 어플리케이션 실행을 완료하는 마지막 쓰레드 역할
- 메인 쓰레드에서 여러 하위 쓰레드를 추가로 시작할 수 있고 하위 쓰레드는 또 여러 하위 쓰레드를 시작할 수 있음
- 자식 쓰레드는 부모 쓰레드의 상태를 상속받음
- 메인 쓰레드가 사용자 쓰레드이기 때문에 하위 쓰레드는 모두 사용자 쓰레드
3.2 사용자 쓰레드
- 메인 쓰레드에서 직접 생성한 쓰레드
- 메인 쓰레드를 포함한 모든 사용자 쓰레드가 종료하게 되면 어플리케이션이 종료
- foreground에서 실행되며 높은 우선순위를 가짐
- JVM은 사용자 쓰레드가 스스로 종료될 때까지 어플리케이션을 강제로 종료하지 않고 대기
- 자바가 제공하는 쓰레드 풀인 ThreadPoolExecutor 통해 사용자 쓰레드 생성
3.3 데몬 쓰레드
- JVM에서 생성한 쓰레드이거나 직접 데몬 쓰레드로 생성한 경우
- 모든 사용자 쓰레드가 작업을 완료하면 데몬 쓰레드의 실행 여부에 관계없이 JVM이 데몬 쓰레드를 강제 종료하고 어플리케이션 종료
- background에서 실행되며 낮은 우선순위를 가짐
- 데몬 쓰레드는 사용자 쓰레드를 지원하는 성격을 가진 쓰레드로서 보통 사용자 작업을 방해하지 않으면서 백그라운드에서 자동으로 작동
- 자바가 제공하는 쓰레드 풀인 ForkJoinPool 통해 데몬 쓰레드 생성
3.3.1 데몬 쓰레드 생성 방법
- setDaemon() 메서드를 통해 사용자 정의 쓰레드를 데몬 쓰레드로 설정 가능
- 디폴트 값은 false 즉, 사용자 쓰레드
- 단, 쓰레드가 실행 중인 동안 setDaemon()을 호출할 경우 IllegalThreadStateException 발생
코드 부연 설명
- 데몬 쓰레드는 종료된 적이 없고 계속 실행 중이지만
- 사용자 쓰레드가 종료되자 JVM은 데몬 쓰레드를 강제 종료하고 어플리케이션 종료
4. ThreadGroup
- 자바는 ThreadGroup을 통해 여러 쓰레드를 그룹화하는 기능 제공
- ThreadGroup에는 다른 ThreadGroup을 포함시킬 수 있고 그룹 내의 모든 쓰레드를 한 번에 종료 및 중단 가능
- 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어야 함
- 명시적으로 쓰레드 그룹에 포함시키지 않을 경우 자신을 생성한 쓰레드가 속해 있는 쓰레드 그룹에 포함됨
- 일반적으로 사용자가 main 쓰레드에서 생성하는 모든 쓰레드는 기본적으로 main 쓰레드 그룹에 속함
4.1 JVM의 쓰레드 그룹 생성 과정
- JVM이 실행되면 최상위 쓰레드 그룹인 시스템 쓰레드 그룹 생성
- JVM 운영에 필요한 데몬 쓰레드들을 생성해서 시스템 쓰레드 그룹에 포함시킴
- 시스템 쓰레드 그룹의 하위 쓰레드 그룹인 메인 쓰레드 그룹을 만들고 메인 쓰레드를 그룹에 포함시킴
- 시스템 에러가 발생하면 메인 쓰레드도 같이 종료되는 이유
코드 부연 설명
- 상위, 하위 그룹 모두 3초간 쓰레드 실행
- 하위 그룹 쓰레드 중지 (subGroupThread, subGroupThread2 모두 종료)
- 상위 그룹만 Running 상태
- 상위 그룹 쓰레드 중지
5. ThreadLocal
- 쓰레드는 오직 자신만이 접근해서 읽고 쓸 수 있는 로컬 변수 저장소 ThreadLocal을 제공
- 각 쓰레드는 고유한 ThreadLocal 객체를 속성으로 가지며 ThreadLocal은 쓰레드 간 격리되어 있음
- 모든 쓰레드가 공통적으로 처리해야 하는 기능이나 객체를 제어해야 하는 상황에서 쓰레드마다 다른 값을 적용해야 하는 경우 사용
- ex) 인증 주체 보관, 트랜잭션 전파, 로그 추적기 등
이미지 부연 설명
- 쓰레드는 ThreadLocal에 있는 ThreadLocalMap 객체를 자신의 threadLocals 속성에 저장
- 쓰레드 생성 시 theadLocals 기본값은 null이며 ThreadLocal에 값을 저장할 때 ThreadLocalMap이 생성되고 threadLocals에 연결
- 쓰레드가 전역적으로 값을 참조할 수 있는 원리는 쓰레드가 ThreadLocal의 ThreadLocalMap에 접근해서 저장된 값을 바로 접근할 수 있기 때문
- ThreadLocalMap은 항상 새롭게 생성되어 쓰레드 스택에 저장되기 때문에 근본적으로 쓰레드 간 데이터 공유가 될 수 없고 이에 따라 동시성 문제 발생 X
5.1 ThreadLocal 사용 시 주의사항
- ThreadPool을 사용하여 쓰레드를 운용할 경우 반드시 ThreadLocal에 저장된 값을 삭제해줘야 함
- 쓰레드 풀은 쓰레드를 재사용하기 때문에 현재 쓰레드가 이전의 쓰레드를 재사용한 것이라면 이전의 쓰레드에서 삭제하지 않았던 데이터를 참조할 수 있기 때문에 문제될 수 있음
코드 부연 설명
- ThreadPool에서 운용하는 쓰레드 개수가 두 개뿐이고 첫 번째 작업에서 threadLocal에 저장된 값을 제거하지 않은 상태
- 두 번째 작업에서는 별도로 ThreadLocal을 설정하지 않았기 때문에 get() 메서드를 통해 값을 가져올 경우 null 값이 출력될 것이라고 예상했지만 첫 번째 작업에서 설정한 값이 출력되는 것을 확인할 수 있음
5.2 ThreadLocal 작동원리
- ThreadLocal은 쓰레드와 ThreadLocalMap을 연결하여 쓰레드 전용 저장소를 구현하고 있는데 이 것이 가능한 이유는 Thread.currentThread()를 참조할 수 있기 때문
- Thread.currentThread()는 현재 실행 중인 쓰레드의 객체를 참조하는 것으로 CPU가 오직 하나의 쓰레드만 할당받아 처리하기 때문에 ThreadLocal에서 Thread.currentThread()를 참조하면 현재 실행 중인 쓰레드의 로컬 변수를 저장하거나 참조할 수 있음
5.3 InheritableThreadLocal
- ThreadLocal의 확장 버전으로서 부모 쓰레드로부터 자식 쓰레드로 값을 전달하고 싶을 경우 사용
- 부모 쓰레드가 InheritableThreadLocal 변수에 값을 설정할 경우 해당 부모 쓰레드로부터 생성된 자식 쓰레드들은 부모의 값을 상속
- 반면, 자식 쓰레드가 상속받은 값을 변경하더라도 부모 쓰레드의 값에는 영향 X
비고
과거에 제가 ThreadLocal 관련하여 작성한 글 참고하시면 좋을 것 같습니다.
https://jaimemin.tistory.com/2007
참고
자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1] - 정수원 강사님
반응형
'JAVA > 비동기 프로그래밍' 카테고리의 다른 글
[Java] 동기화 기법 (1) | 2024.03.04 |
---|---|
[Java] 동기화 개념 (0) | 2024.02.24 |
[Java] Java 쓰레드 기본 API (0) | 2024.02.05 |
[Java] 쓰레드 생성 및 실행 구조 (0) | 2024.01.31 |
[OS] 운영체제 기초 (0) | 2024.01.19 |