JAVA/비동기 프로그래밍

[Java] Java 쓰레드 활용

꾸준함. 2024. 2. 21. 19:11

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) 인증 주체 보관, 트랜잭션 전파, 로그 추적기 등

 

https://www.cnblogs.com/iiiDragon/p/9815951.html

 

 

이미지 부연 설명

  • 쓰레드는 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

 

[SpringBoot] ThreadLocal 간단 정리

개요 스프링 프레임워크 내 Bean들은 스프링 컨테이너에 의해 싱글톤으로 관리됩니다. 정리하자면 인스턴스가 단 하나만 존재한다는 것인데 여러 쓰레드가 동시에 해당 인스턴스를 접근할 경우

jaimemin.tistory.com

 

참고

자바 동시성 프로그래밍 [리액티브 프로그래밍 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