본문 바로가기

Java/병렬 프로그래밍

자바 병렬 프로그래밍 - 활동성을 최대로 높이기

 

안전성과 활동성 사이에는 밀고 당기는 관계가 있다.

 

안전성 확보를 위해 락을 사용하는데 , 락 순서에 따라 데드락이 발생하기도 한다.

락과 비슷하게 자원 사용량을 제한하고자 스레드풀이나 세마포어를 사용하기도 하는데,

구조를 이해하지 못하고 있담년 자원을 할당 받지 못하는 데드락이 발생하기도 한다.

 

이번장에서는 데드락 처럼 활동성에 문제를 주는 상황과 방지하는방법을 알아보자

 

데드락

'식사하는 철학자' 로 널리 알려져있다.

젓가락을 양쪽에 한짝씩 놓고 음식을 먹을땐 양쪽 걸 가져와서 먹게되는데

모두가 왼쪽을 가지고 있으면 오른쪽 젓가락을 가지려 할때 아무도 먹지 못하는 데드락 상황이 발생한다.

 

스레드 A 가 특정 락 L 을 잡고 있고 두번째 락M을 기다리고 있는 상태에서

스레드 B 가 M 을 잡고 있고 L 을 원하고 있으면 데드락이 발생한다.

 

db 시스템은 데드락을 검출 한 후 복구하는 기능을 갖고있다.

트랜잭션을 하다보면 여러개의 락이 필요할 수 있는데, 커밋 될 때까지 풀리지 않는다.

따라서 흔치 않지만 두개의 트랜잭션이 서로 원하는 락을 필요하면 데드락이 발생 할 수도 있는데

이때 db 는 하나의 트랜잭션을 종료 시켜버려서 다른 트랜잭션 락을 확보한다.

 

하지만 JVM 은 db 처럼 데드락 상태를 추적 하는 기능은 갖고 있지 않다.

따라서 데드락이 발생하면 끝이다.

프로그램을 강제 종료 시키기 전까지 영원히 멈추게 된다.

어떤 스레드인지에 따라 어플리케이션 자체가 멈출 수 있다. 

 

데드락은 서비스를 시작하고 나서 부하가 걸리는 경우와 같이 최악의 상황에서 모습을 드러낸다.

 

- 락 순서에 의한 데드락

 

락 순서에 의한 데드락

 

문제가 되는 실행 순서

해당 상황이 오면 데드락이 발생한다.

서로 다른 순서로 동일한 락을 확보하려 하기 때문이다.

 

프로그램 내부 모든 스레드에서 모두 같은 순서로만 락을 사용한다면 락 순서에 의한 데드락은 발생하지 않는다.

 

락을 공유하는 순간에 데드락을 방지하려면 어떤 순서로만 실행하는 걸로는 충분치 않고

오른손이 하는일을 왼손이 알고있어야 한다.

 

 

- 동적인 락 순서에 의한 데드락

 

이 코드는 계좌에 이체하기전에 양쪽 계좌에 대한 락을 확보하는 코드다

어떤때 이 코드에서 데드락이 걸릴까

서로 다른 스레드가 계좌 순서를 바꿔 락을 확보하려고 할때 데드락이 걸릴 수 있다.

이것도 동일한 방법으로 검출 할 수 있다.

락의 순서를 프로그램에서 제어 할 수 있으면 데드락을 방지 할 수 있다.

 

System.identityHashCode 를 사용하면 순서를 부여 할 수 있다.

락을 순서대로 확보하는 모습

 

- 객체 간의 데드락

 

 두개의 락을 모두 사용하는 메소드가 없지만,

setLocation 과 getImage 를 호출 하는 클래스는 두개의 락을 쓰는 셈이다.

 

위치 값을 setLocation 에 넘기면 택시는 값을 업데이트 하고 목적지에 도착했으면

Dispatcher 에 새로운 목적지를 알려달라고 요청한다.

즉, Taxi 락을 확보하고 Dispatcher 락을 확보하는데,

getImage 를 호출하면 Dispatcher 락을 확보하고 Taxi 락을 확보하게 되어서 

데드락이 발생한다.

 

- 오픈 호출

 

락을 전혀 확보하지 않은 상태에서 메소드를 호출하는 것을 오픈 호출 이라고 하고

오픈 호출로만 이뤄진 클래스는 락을 확보한 채로 메소드를 호출 하는 클래스보다 안정적이다.

 

오픈 호출을 사용하는건 스레드 안전성을 확보하기 위해 캡슐화 기법을 사용하는 것과 비슷하다.

 

오픈 호출을 사용하는 모습

synchronized 블록의 범위를 최대한 줄여 직접 관련된 부분에서만 락을 확보하도록 하는 작업

편하다고 메소드에 synchronized 를 걸지 말자.

 

 

- 리소스 데드락

 

스레드가 이미 확보하고 있는 락을 놓지 않아서 데드락이 걸리는것처럼

필요한 자원을 사용하기 위해 대기 하는 과정에도 데드락이 발생할수있다.

 

예를들어 자원 풀은 풀이 비어있을때 풀 내부의 자원을 달라고 요청하는 객체가

대기하도록 세마포어를 구현하는 경우가 많다.

근데 특정 작업이 db 연결 2개를 확보해야 하는데

스레드 A 가 1번 db를 확보하고 2번 db 를 확보하고

B는 2번을 확보하고 1번을 확보한다고 칠때

풀의 크기가 작으면 연결을 확보하려고 할때 데드락이 발생할 수 있다.

 

다른 예로는 스레드 부족 데드락인데 ,단일 스레드에서 현재 작업이 다른 작업을 큐에 쌓고

그 작업이 끝날떄까지 기다리는 데드락 상황 이있 다.

 

 

데드락 방지 및 원인 추적

 

한번에 하나 이상의 락을 사용하지 않는 프로그램은 데드락이 발생 하지 않는다.

최대한 한번에 하나 이상의 락을 사용하지 않게 만드는게 좋다.

 

여러개의 락을 써야 한다면 최대한 함께 동작하는 부분을 줄여야 한다.

 

세세하게 작업하기 위해선

첫번째로 여러개의 락을 확보해야 하는 부분이 어딘지 찾고

다음으로는 분석 작업을 진행해 어디에서건 락을 지정된 순서에 맞게 쓰게 해야한다.

가능한 곳에서는 오픈 호출 방법을 이용하면 분석과 확인이 편해진다.

 

- 락의 시간 제한

 

데드락에서 검출하고 복구하는 방법으로 synchronized 같은 암묵적인 락을 사용하는 대신

Lock 클래스의 메소드 가운데 시간을 제한 할 수 있는 tryLock 을 사용하는 방법이 있다.

특정 시간동안 락을 확보 못하면 오류 발생시키도록 한다.

 

 

- 스레드 덤프를 활용한 데드락 분석

 

데드락 방지가 최우선 목표지만,

일단 방생하게 되면 JVM 이 만들어낸 스레드 덤프를 활용해 데드락이 발생한 위치를 확인하는데 도움이 된다.

 

스레드 덤프에는 실행 중인 모든 스레드의 스택 트레이스가 담겨 있다.

락과 관련된 정보도 담겨 있어서 각 스레드가 어떤 락을 확보하는지, 스택의 어느 부분에서 락을 확보했는지

대기 중인 스레드가 어느 락을 확보하려고 대기 중이었는지 에 대한 정보를 갖고있다.

 

J2EE 어플리케이션에서 뽑아낸 스레드 덤프의 일부분이다.

데드락 오류를 발생시킨 원인에 3개의 컴포넌트, J2EE 어플리케이션 , J2EE 컨테이너 , JDBC 드라이버가 관련돼 있음을 알수있다.

 

스레드 하나가 MumbleDBConnection 에 대한 락을 확보하고 있고 ,

MumbleDBCallableStatement 에 대한 락을 확보하려고 대기하고 있다.

또 다른 스레드는 MumbleDBCallableStatement 를 갖고 MumbleDBConnection 를기다리고있다.

 

위 스레드 덤프를 보면 JDBC 드라이버에 락 순서가 일정하지 않다는 버그가 있다.

 

그 밖의 활동성 문제점

 

활동성을 떨어뜨리는 주된 원인은 데드락이지만, 

소모, 놓친 신호, 라이브락 등 다양한 원인을 마주치게 된다.

 

- 소모 

소모 상태는 스레드가 작업을 진행하는데 필요한 자원을 영영 할당 받지 못했을 경우에 발생한다.

소모 상태를 일으키는 흔한 원인은 CPU 다

소모 상황이 발생하는 원인은 대부분 스레드의 우선순위를 올리거나 내리는 부분에 있다.

 

- 스레드의 우선순위를 변경하고 싶어도 참아라. 대부분의 병렬 어플리케이션은 모든 스레드의 우선 순위에 

기본값을 사용하고 있다.

 

- 형편 없는 응답성

 

어플리케이션의 응답성이 떨어진다면 락을 제대로 관리하지 못하는 것이 원인일 수 있다.

특정 스레드가 대량의 데이터를 넘겨받아 처리하느라 필요 이상으로 긴 시간 동안 락을 확보하고 있다면

넘겨준 대량의 데이터를 사용해야 하는 다른 스레드는 상당히 긴 시간 동안 대기해야한다.

 

 

- 라이브락

 

특정 작원의 결과를 받아야 다음 단계로 넘어갈 수 있는데 실패를 계속해서 재시도하는 경우에 쉽게 찾아볼수있다.

라이브락은 회복 불가능한 오류를 계속 회복하려고 할때 발생한다.

라이브락은 재시도 시 약간의 로직을 넣어 해결하는 방법이 있다.

ex) 재시도 할때까지 잠시 대기 했다가 다시 실행

 

 

활동성 문제는 심각한 경우가 많은데,

문제를 해결하려면 일반적으로 어플리케이션을 종료하는것 외에는 방법이 없다는데 심각성의 원인이 있다.

가장 흔한 활동성 문제는 락 순서에 의한 데드락이다.

락 순서에 의한 데드락을 방지하려면, 어플리케이션 설계부터 여러개의 락을 사용하는 부분에 충분히 생각해야한다.

어플리케이션 내부의 스레드에서 두개 이상의 락을 사용한다면, 일정한 순서를 두고 여러개의 락을 확보해야한다.

효과적인 해결방법은 오픈 호출이다.