JAVA/RxJava

[Java] synchronized, wait() & notify(), volatile, Deadlock

꾸준함. 2024. 3. 5. 20:30

개요

앞선 게시물을 읽고 오시는 것을 추천드립니다!

https://jaimemin.tistory.com/2392

 

[Java] 동기화 개념

1. 싱글 쓰레드 vs 멀티 쓰레드 프로세스는 오직 한 개의 쓰레드로만 구성하는 싱글 쓰레드 프로세스와 하나 이상의 쓰레드로 구성하는 멀티 쓰레드 프로세스로 구분할 수 있습니다,. 멀티 쓰레

jaimemin.tistory.com

https://jaimemin.tistory.com/2406

 

[Java] 동기화 기법

개요 앞선 게시물인 [Java] 동기화 개요를 읽고 해당 게시물을 읽는 것을 추천드립니다. 1. 뮤택스 (Mutual Exclusion) 공유 자원에 대한 경쟁 상태를 방지하고 동시성 제어를 위한 락 메커니즘 쓰레드

jaimemin.tistory.com

 

Synchronized 

  • 자바는 synchronized 구문을 통해 모니터 영역을 동기화
  • synchronized는 자바에 내장된 락으로 암묵적인 락(Intrinsic Lock) 혹은 모니터 락(Monitor Lock)이라고 지칭
  • synchronized은 같은 Mutex를 공유하므로 동일한 모니터를 가진 객체에 대해 오직 하나의 쓰레드만 임계 영역에 접근할 수 있도록 보장
  • 쓰레드가 synchronized 블록에 들어가기 전에 락이 자동 확보되며 어떠한 이유에서든 해당 블록을 벗어날 때 락이 자동 해제

 

synchronized 키워드는 모니터 락을 활용하여 동기화를 지원하는데, 이를 위해 네 가지 방법을 제공합니다.

  • synchronized 인스턴스 메서드
  • synchronized 정적 메서드
  • synchronized 인스턴스 블록
  • synchronized 정적 블록

 

위에 열거한 네 가지 방식은 크게 메서드 동기화 방식과 블록 동기화 방식으로 구분할 수 있습니다.

 

1. 메서드 동기화 방식

  • 메서드 전체가 임계 영역이므로 메서드 내 모든 코드를 동기화
  • 동시성 문제를 한 번에 제어할 수 있는 것이 장점
  • 메서드 내 코드의 세부적인 동기화 구조를 가지기 어렵고 동기화 영역이 클 경우 성능 저하 야기할 수 있다는 점이 단점
  • 세부적으로 인스턴스 메서드 동기화 방식과 정적 메서드 동기화 방식으로 나뉨

 

1.1 인스턴스 메서드 동기화 방식

  • 동일한 인스턴스 내 synchronized가 적용된 곳은 하나의 락을 공유
  • 인스턴스가 여러 개일 경우 인스턴스 별로 모니터 객체를 가짐
    • 모니터 별로 락을 획득해서 동기화 영역에 진입하고 빠져나올 때 락 해제 가능
    • 다만, 같은 클래스 내 선언된 인스턴스 메서드들의 경우 동일한 모니터인 this를 공유
    • 인스턴스 메서드는 this가 모니터

 

1.2 클래스 메서드 동기화 방식

  • 클래스 단위로 모니터가 동작하며 synchronized가 적용된 곳은 하나의 락 공유
  • 클래스는 메모리에 오직 하나만 존재하므로 하나의 모니터를 공유해서 동기화할 때 사용
  • 정적 메서드는 클래스가 모니터

 

2. 블록 동기화 방식

  • 특정 블록을 정해 임계 영역으로 구성하기 때문에 블록 내 코드만 동기화
  • 세부적으로 임계 영역을 정해 필요한 블록만 동기화 구조를 가질 수 있고 동기화 영역이 작고 효율적인 구성이 가능해 성능 저하가 덜하다는 것이 장점
  • 세부적으로 인스턴스 블록 동기화 방식과 정적 블록 동기화 방식으로 나뉨

 

2.1 인스턴스 블록 동기화 방식

  • 모든 인스턴스가 모니터를 가지기 때문에 모니터를 여러 인스턴스로 구분해서 동기화 구성 가능 (this)
  • 클래스의 인스턴스가 여러 개일 경우 인스턴스 별로 모니터 객체를 가짐
    • 쓰레드는 모니터 별로 락 획득 후 동기화 영역을 진입하고 빠져나올 때 락 반납

 

2.2 정적 블록 동기화 방식

  • 클래스 단위로 모니터가 동작하여 synchronized 키워드가 적용된 곳은 하나의 락을 공유
  • 모든 클래스가 모니터를 가지기 때문에 모니터를 여러 클래스로 구분해서 동기화 구성 가능

 

 

 

부연 설명

  • synchronizedBlock 메서드 내 object가 this일 경우 syncMethod와 같은 모니터를 공유하기 때문에 동시 접근 불가
  • 마찬가지로 staticSynchronizedBlock 메서드 내 클래스와 staticSyncMethod를 선언한 클래스가 동일할 경우 같은 모니터를 공유하기 때문에 동시 접근 불가
  • 정리하자면 쓰레드 간 객체의 메서드를 동기화하기 위해서는 쓰레드는 같은 뮤택스 즉, 같은 객체의 모니터를 참조하고 있어야 함

 

Synchronized 특성

 

synchronized 키워드는 다음의 특성을 가집니다.

  • 모니터 내 이미 synchronized 영역에 들어간 쓰레드는 다시 같은 모니터 영역에 들어갈 수 있고 이를 "모니터 재진입"이라고 지칭
    • 락의 획득이 호출 단위가 아니라 쓰레드 단위로 일어나기 때문에 가능
    • 이미 락을 획득한 쓰레드는 같은 락을 얻기 위해 대기할 필요 없이 synchronized 블록을 만났을 때 같은 락을 확보하고 진입
    • 동기화된 메서드에서 다른 동기화 된 메서드 호출 시 이미 락을 가지고 있는 쓰레드가 같은 락을 확보하고 재진입 시 정상적으로 진행 (데드락 발생 X)

 

  • synchronized는 가시성을 지원
    • 가시성은 한 쓰레드에서 수정된 변수의 변경이 다른 스레드에서 즉시 반영되도록 하는 특성을 의미
    • synchronized를 사용하면 이러한 가시성이 유지되어 여러 쓰레드 간에 공유된 데이터의 일관성을 보장

 

  • wait() 메서드 호출 시 락을 반납하는 반면 sleep() 메서드 실행 시 쓰레드는 동기화 영역에서 대기 중이더라도 획득한 락을 놓거나 해제 X
  • synchronized의 동기화 영역에 진입하지 못하고 대기 중인 쓰레드들끼리 락을 획득하기 위해 경쟁할 때 순서 정해져 있지 않음 (FIFO가 아님)
    • OS 스케줄러가 적절히 조율해 starvation 현상 자체적으로 해결

 

wait() & notify()

  • wait(), notify(), notifyAll() 메서드는 모니터 객체의 조건 변수와 함께 사용해 동기화를 구현할 수 있는 동기화 메커니즘
    • 뮤택스 동기화 기법만으로는 충족되지 않는 동기화 문제를 해결할 수 있는 협력(cooperation)에 의한 동기화 장치
    • wait(), notify(), notifyAll() 메서드는 락을 확보한 상태일 때 작동하기 때문에 반드시 synchronized 블록 안에서만 사용해야 함

 

  • wait(), notify(), notifyAll() 메서드는 최상위 계층인 Object 내 선언되어 있고 모든 클래스는 암묵적으로 Object를 상속하므로 모든 클래스에서 해당 메서드들을 호출 가능

 

1. wait()

 

  • 메서드 호출 한 쓰레드를 대기 상태로 전환시키고 모니터 락은 해제되며 다른 쓰레드가 모니터 락을 획득하여 작업 수행
  • 조건 변수와 함께 사용되어 특정 조건이 만족될 때까지 대기
  • 다른 쓰레드가 동일한 모니터 락을 획득하고 notify() 혹은 notifyAll() 메서드 호출 시 대기 중인 쓰레드 혹은 쓰레드들이 Wait Set에서 Entry Set으로 이동
  • Entry Set으로 이동한 쓰레드가 경쟁을 통해 락을 획득하면 wait() 다음 구문부터 수행
  • wait(long timeout)을 사용하여 일정 시간 동안 대기하도록 타임아웃을 지정 가능하며 지정한 시간 경과 시 쓰레드는 자동으로 깨어남
  • 대기 중인 쓰레드에 interrupt 신호가 전달되면 InterruptedException이 발생하며 interrupt 된 쓰레드는 대기에서 깨어남
    • 이를 위한 적절한 예외처리 필요

 

2. notify() & notfiyAll()

 

  • 같은 모니터의 조건 변수에서 대기 중인 쓰레드(들)을 대상으로 호출하는 메서드
    • notify() 실행 시 임의로 하나를 깨우고
    • notifyAll() 실행 시 쓰레드 전체 깨움
    • 일어난 쓰레드는 Entry Set으로 넘어가 락 획득 위해 경쟁
    • 쓰레드를 깨울 때 우선순위가 높은 쓰레드가 깨어날 것이라는 보장은 없고 순전히 OS와 JVM의 스케줄링 정책에 따라 결정

 

  • 자바는 signal & continue 조건 변수 정책을 따르기 때문에 notify() 메서드 호출한 쓰레드는 synchronized 블록이 끝나기 전까지 락이 해제되지 않으며 해당 블록에서 빠져나가야 락 해제

 

 

 

volatile

  • volatile은 변수의 가시성과 연산의 순서를 제어하기 위해 사용하는 키워드
  • 쓰레드 간의 데이터 일관성과 가시성을 보장하는 역할

 

1. volatile 키워드가 필요한 이유

 

1.1 가시성

  • CPU는 값을 읽어올 때 우선 캐시에 해당 값이 있는지 확인하고 없는 경우에만 메인 메모리에서 읽어오는 특성을 가짐
  • 멀티 쓰레드 환경에서 CPU에 할당된 쓰레드가 메인 메모리가 아닌 CPU 캐시로부터 공유 변수를 참조하게 되면 서로 다른 변수 값을 쓰레드가 참조하게 되는 상황 발생
  • 앞서 언급했다시피 volatile은 변수의 가시성을 보장하는데 가시성은 한 쓰레드에서 수정된 변수의 변경이 다른 스레드에서 즉시 반영되도록 하는 특성을 의미
    • 멀티 쓰레드 프로그래밍에서는 여러 쓰레드가 동시에 변수에 접근하고 수정할 수 있기 때문에 모든 쓰레드에게 변수의 값이 일관되게 보여지도록 가시성 확보 필요
    • volatile 키워드가 적용된 변수는 쓰레드 간 가시성을 확보하기 위해 CPU가 작업한 결과를 즉시 메인 메모리에 반영하며 쓰레드는 캐시가 아닌 메인 메모리에서 값을 참조

 

1.2 Happens-Before 보장

  • JVM은 프로그램의 성능을 향상하기 위해 명령어를 재정렬
    • 하지만 동기화 블록에서는 명령어 순서가 변할 경우 결과가 바뀔 수 있음
    • volatile 변수를 사용하면 해당 변수를 읽거나 쓰는 작업은 재정렬되지 않도록 보장

 

2. volatile의 한계점

 

  • volatile은 쓰레드 간 공유 변수에 대한 가시성을 보장하지만 mutex를 보장해주지는 않음
  • 쓰기 작업하는 쓰레드가 한 개이고 읽기 작업하는 쓰레드가 N개일 때는 volatile이 효과적이지만
  • 쓰기 작업하는 쓰레드가 N개일 경우 동시성을 보장해주지 못함

 

3. volatile의 한계점을 보완하는 synchronized

 

  • synchronized 블록을 사용하면 한 시점에 오직 하나의 쓰레드만이 동기화 영역에 접근할 수 있도록 보장 (mutex)
  • synchronized 블록 안에서 참조되는 모든 변수들은 메인 메모리로부터 읽어 들여지고 블록을 벗어나면 그동안 수정된 모든 변수들이 즉시 메인 메모리로 반영 (volatile)
  • 이처럼 synchronized는 상호배제와 함께 가시성의 문제까지 해결하는 기능을 포함
    • synchronized 블록 내에는 volatile 키워드 없어도 됨
    • trade-off로 성능 저하가 있을 수 있음

 

Deadlock

  • 프로세스나 쓰레드들이 서로가 소유하고 있는 자원을 기다리며 무한히 대기하고 있는 상태
    • 아무런 진전도 이루어지지 않아 작업이 진행되지 않는 문제 발생
    • 동일한 환경과 코드에서 발생할 수도 있고 발생하지 않을 수도 있기 때문에 완전히 예방하는 것은 불가능하며 적절한 대응 필요
    • ex) 식사하는 철학자 문제

 

1. Deadlock 발생 조건

 

데드락은 다음의 네 가지 필요조건을 모두 만족할 때 발생합니다.

달리 말하자면 한 가지 조건이라도 만족하지 않으면 데드락이 발생하지 않습니다.

  • 상호 배제
  • 점유 대기
  • 비선점
  • 순환 대기

 

1.1 상호 배제

  • 자원은 한 번에 하나의 쓰레드만 사용 가능
  • ex) 철학자는 한 번에 하나의 젓가락을 사용할 수 있으므로 상호 배제를 구현하여 젓가락을 동시에 두 명 이상의 철학자가 사용하지 못 함

 

1.2 점유 대기

  • 쓰레드가 최소한 하나의 자원을 보유한 상태에서 다른 자원을 기다리고 있음
  • ex) 철학자들은 젓가락을 기다리는 상태에서 무한히 대기할 수 있으며, 다른 철학자가 젓가락을 반납하기를 기다림

 

1.3 비선점

  • 자원을 할당받은 쓰레드가 자원을 스스로 반납하기 전까지 자원을 강제로 뺏을 수 없음
  • ex) 철학자들은 젓가락을 사용하는 동안 다른 철학자에 의해 젓가락을 빼앗길 수 없으며 젓가락은 해당 철학자가 반납할 때까지 해당 철학자가 보유

 

1.4 순환 대기

  • 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있어 사이클 형성
  • ex) 각 철학자는 서로 다른 두 개의 젓가락을 사용해야 하므로 순환적으로 다음 철학자가 요구하는 젓가락을 가지고 있을 수 있어 사이클 형성

 

2. 데드락 예시

 

2.1 락 순서에 의한 데드락

 

 

 

 

코드 부연 설명

  • Thread-0 쓰레드는 lock2가 해제되길 기다리고
  • Thread-1 쓰레드는 lock1이 해제되길 기다리는 중

 

2.2 객체 간의 데드락

 

 

코드 부연 설명

  • Thread-1 쓰레드는 resourceA, Thread-2는 resourceB의 모니터를 소유
  • Thread-1 쓰레드는 resourceB, Thread-1은 resourceA의 모니터 해제를 기다림

 

3. Deadlock 방지와 원인 추적

 

3.1 Deadlock 방지

  • 일단 데드락이 발생하면 애플리케이션 단에서는 대응이 어렵고 서버 재기동 혹은 종료 밖에 해결책이 없음
  • 데드락 발생 조건인 네 가지 조건 중 최소한 1개를 방지함으로써 데드락 방지 가능
    • 한 번에 하나 이상의 락을 사용하지 않는 것을 권장
    • 락의 순서를 적절히 조정
    • 락 타임아웃 설정
    • 메서드는 오픈 호출 형태로 구현

 

3.1.1 한 번에 하나 이상의 락을 사용하지 않는 것을 권장

  • 데드락은 쓰레드가 락을 중첩으로 제어하면서 발생하는 경우가 대다수
  • 가능한 한 쓰레드가 두 개 이상의 락을 제어하는 상황을 만들지 않도록 하는 것을 권장

 

3.1.2 락의 순서를 적절히 조정

  • 불가피하게 여러 개의 락을 사용해야 한다면 락의 점유 순서를 일정한 순서로 정해주도록 함으로써 데드락이 발생할 수 있는 조건 중 하나인 순환 대기 방지 가능
  • 2.1 코드의 해결책
    • processA() 메서드와 processB() 메서드 모두 lock1을 먼저 획득한 이후 lock2를 획득하도록 순서 조정

 

 

3.1.3 락 타임아웃 설정

  • 락을 요청할 때 일정 시간 이내 락 획득 못하면 다른 작업을 수행하도록 타임아웃 설정
  • 락 획득에 타임아웃 오류가 나면 오래 기다리지 않고 제어권이 다시 돌아오기 때문에 현재 소유한 락을 해제하고 잠시 대기 후 데드락 상황이 지나가면 다시 정상으로 동작

 

 

 

3.1.4 메서드는 오픈 호출 형태로 구현

  • 락을 전혀 확보하지 않은 상태에서 메서드를 호출하는 것을 오픈 호출
    • synchronized 메서드 방식이 아닌 synchronized 블록 방식
    • 락이 필요한 임계 영역만 보호하도록 블록 지정

 

  • 여러 개의 락을 호출하더라도 동시에 락을 점유하는 것이 아닌 순차적으로 락을 획득하고 해제하는 방식으로 메서드 호출

 

3.2 Deadlock 원인 추적

 

쓰레드 덤프를 활용하면 데드락 원인을 파악할 수 있습니다.

  • 쓰레드 덤프에는 실행 중인 쓰레드의 모든 stack trace가 담겨 있고 락과 관련된 정보도 포함되어 있음
  • 이를 활용해 어디에서, 어떤 쓰레드가, 어느 시점에, 왜 데드락이 발생했는지 원인 추적 및 분석 가능

 

참고

자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1] - 정수원 강사님

반응형

'JAVA > RxJava' 카테고리의 다른 글

[Java] 동기화 도구  (0) 2024.03.14
[Java] Lock, ReentrantLock, ReadWriteLock, ReentrantReadWriteLock  (0) 2024.03.09
[Java] 동기화 기법  (1) 2024.03.04
[Java] 동기화 개념  (0) 2024.02.24
[Java] Java 쓰레드 활용  (1) 2024.02.21