본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java Concurrency] 활동성 최대로 높이기 #1

by 돼지왕 왕돼지 2017. 5. 1.
반응형

 [Java Concurrency] 활동성 최대로 높이기 #1


executor, hashcode, identityhashcode, java concurrency, jvm, liveness, object, open call, SAFETY, synchronized, system.identityhashcode, tie-breaking, [Java Concurrency] 활동성을 최대로 높이기 #1, 가용성, 강제종료, 객체 간의 데드락, 객체 순서, 단일 스레드, 단일성, 대기, 데드락, 데이터베이스 시스템, 락, 락 순서, 리소스 데드락, 문법, 불변, 비교 가능, 서버, 세마포어, 스레드 부족, 스레드 부족 데드락, 스레드 안전, 스레드 안정성, 스레드 풀, 시스템 자원, 안정성, 앱, 앱 종료, 에릴리언 메소드, 에일리언 메소드, 오픈 호출, 위험도, 자바 가상 머신, 캡슐화, 크리티컬 섹션, 타이 브레이킹, 트랜잭션, 파급, 필요한 자원, 활동성, 활동성 최대로 높이기, 희생양


-
안정성(safety)와 활동성(liveness) 사이에는 밀고 당기는 힘이 존재하는 경우가 많다.
스레드 안전성을 확보하기 위해서 락을 사용하곤 하는데, 락이 우연찮게 일정한 순서로 동작하다 보면 락 순서에 따라 데드락이 발생하기도 한다.

시스템 자원 사용량을 적절한 수준에서 제한하고자 할 때 스레드 풀이나 세마포어를 사용하기도 하는데, 동작하는
구조를 정확하게 이해하지 못하고 있다면 더 이상 자원을 할당받지 못하는 또 다른 형태의 데드락이 발생할 수 있다.


-
자바 어플리케이션은 데드락 상태에서 회복할 수 없기 때문에
항상 프로그램의 실행 구조상 데드락이 발생할 가능성이 없는지 먼저 확인해야 한다.



10.1. 데드락


-
데이터베이스 시스템은 데드락을 검출한 다음 데드락 상황에서 복구하는 기능을 갖추고 있다.
데이터베이스 서버가 트랜잭션 간에 데드락이 발생했다는 사실을 확인하고 나면,
데드락이 걸린 트랜잭션 가운데 희생양을 하나 선택해 해당 트랜잭션을 강제 종료시킨다.


-
자바 가상 머신(JVM) 은 데드락 상태를 추적하는 기능은 갖고 있지 않다.
따라서 만약 자바 프로그램에서 데드락이 발생하면 그 순간 게임은 끝이다.


-
데드락이 걸린 스레드가 뭘 하는 스레드이냐에 따라 앱 자체가 완전히 멈춰버릴 수도 있고,
아니면 멈추는 범위가 줄어 일부 모듈만 동작을 멈출 수도 있고,
아니면 전체적인 성능이 떨어지는 정도의 영향을 미칠 수도 있다.


-
데드락 상태에서 앱을 정상적인 상태로 되돌릴 수 있는 방법은
앱을 종료하고 다시 실행하는 것밖에 없고,
다시는 같은 데드락이 발생하지 않기를 바라는 수밖에 없다.



* 10.1.1. 락 순서에 의한 데드락

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



* 10.1.2. 동적인 락 순서에 의한 데드락

-
모든 스레드가 락을 동일한 순서로 확보하려 할 때 데드락이 발생할 수 있는데,
여기에서 락을 확보하는 순서는 인자의 순서에 달릴 수 있다.

public void transferMoney( Account fromAcc, Account toAcc ){

synchronized( fromAcc ){

synchronized( toAcc ){

...

}

}

}



-
락을 확보하려는 순서를 내부적으로 제어할 수 없기 때문에 여기에서 데드락을 방지하려면
락을 특정 순서에 맞춰 확보하도록 해야 하고, 락을 확보하는 순서를 프로그램 전반적으로 동일하게 적용해야 한다.


-
객체에 순서를 부여할 수 있는 방법 중 하나는 바로 System.identityHashCode 를 사용하는 방법이다.
identityHashCode 메소드는 해당 객체의 Object.hashCode 메소드를 호출했을 때의 값을 알려준다.

거의 발생하지 않는 일이지만 두 개의 객체가 같은 hashCode 값을 갖고 있는 경우에는
락 순서가 일정하지 않을 수 있다는 문제점을 제거하려면 세 번째 타이 브레이킹(tie-braking) 락을 사용하는 방법이 있다.

private static final Object tieLock = new Object();


int fromHash = System.identityHashCode( fromAcc );

int toHash = System.identityHashCode( toAcc );


if ( fromHash < toHash ){

synchronized( fromAcc ){

synchronized( toAcc ){

...

}

}

} else if ( fromHash > toHash ){

synchronized( toAcc ){

synchronized( fromAcc ){

...

}

}

} else{

synchronized( tieLock ){

synchronized( fromAcc ){

synchronized( toAcc ){

...

}

}

}

}



-
Account 클래스 내부에 계좌 번호와 같이 유일하면서 불변이고 비교도 가능한 값을 키로 갖고 있다면
한결 쉬운 방법으로 락 순서를 지정할 수 있다.
Account 객체를 그 내부의 키를 기준으로 정렬한 다음 정렬한 순서대로 락을 확보한다면
타이 브레이킹 방법을 사용하지 않고도
전체 프로그램을 통털어 계좌를 사용할 때 락이 걸리는 순서를 일정하게 유지할 수 있다.



* 10.1.3. 객체 간의 데드락

두 개의 락을 모두 사용하지 않으면서도,
각기 다른 락을 잡고 있는 메소드 둘을 호출하는 메소드는 두 개의 락을 사용하는 셈이다.


-
데드락 발생했는지를 볼 때는..
락을 확보한 상태에서 에일리언 메소드를 호출하는지 확인하면 도움이 된다.


-
락을 확보한 상태에서 에일리언 메소드를 호출한다면 가용성에 문제가 생길 수 있다.
에일리언 메소드 내부에서 다른 락을 확보하려고 하거나, 아니면 예상하지 못한 만큼 오랜 시간 동안 계속해서 실행된다면
락이 필요한 다른 스레드가 계속해서 대기해야 하는 경우도 생길 수 있다.



* 10.1.4. 오픈 호출

-
당연한 이야기이지만 한개의 락을 사용하는 각 객체들을 사용하는 함수는 자신이 데드락을 유발시켰는지 알지 못하며, 알지 못해야만 한다.
메소드 호출이라는 것이 그 너머에서 어떤 일이 일어나는지 모르게 막아주는 추상화 방법이기 때문이다.
하지만 호출한 메소드 내부에서 어떤 일이 일어나는지 알지 못하기 때문에
특정 락을 확보한 상태에서 에일리언 메소드를 호출한다는 건 파급 효과를 분석하기가 굉장히 어렵고, 따라서 위험도가 높은 일이다.


-
락을 전혀 확보하지 않은 상태에서 메소드를 호출하는 것을 오픈 호출(open call)이라고 하며,
메소드를 호출하는 부분이 모두 오픈 호출로만 이뤄진 클래스는 락을 확보한 채로 메소드를 호출하는 클래스보다 훨씬 안정적이며 다른 곳으로 불러다 쓰기도 좋다.
데드락을 미연에 방지하고자 오픈 호출을 사용하는 것은 스레드 안전성을 확보하기 위해 캡슐화 기법을 사용하는 것과 비슷하다고 볼 수 있다.

캡슐화 기법을 전혀 사용하지 않고도 스레드 안전한 프로그램을 작성할 수는 있지만,
캡슐화 기법을 사용해 작성한 프로그램과 비교할 때 스레드 안전성을 분석하는 일이 훨씬 어려울 수 있다.


-
문법이 간결하다거나 사용하기 편하다는 이유로 꼭 필요한 부분에서만 synchronized 블록을 사용하는 대신 습관적으로 메소드 전체에 synchronized 구문으로 동기화를 걸어주는 것이 데드락의 원인일 수 있다.


-
프로그램을 작성할 때 최대한 오픈 호출 방법을 사용하도록 한다.
내부의 모든 부분에서 오픈 호출을 사용하는 프로그램은 락을 확보한 상태로 메소드를 호출하곤 하는 프로그램보다 데드락 문제를 찾아내기 위한 분석 작업을 훨씬 간편하게 해준다.


-
synchronized 블록의 구조를 변경해 오픈 호출 방법을 사용하도록 하면
원래 단일 연산으로 실행되던 코드를 여러 개로 쪼개 실행하기 때문에 예상치 못했던 상황에 빠지기도 한다.


-
연산의 단일성을 잃는다는 것이 일부 상황에서는 큰 문제가 되기도 한다.
연산의 단일성을 확보하는 방법 가운데 하나는 오픈 호출된 이후에 실행될 코드가 한 번에 단 하나씩만 실행되도록 객체의 구조를 정의하는 방법이다.

예를 들어 어떤 서비스를 종료할 때 현재 실행되고 있는 작업이 완료될 때까지 대기하려 할 것이고,
완료된 이후에 서비스에서 사용하던 자원을 모두 반환하려 할 것이다.
이 때 서비스의 상태를 "종료 중" 이라고 설정할 동안만 락을 쥐고 있으면서 다른 스레드가 새로운 작업을 시작하거나 아니면 서비스 종료 절차를 시작하지 못하도록 미리 예방해두는 방법이다.
그러면 종료 절차가 모두 끝날 때까지 대기할 것이고, 오픈 호출이 모두 끝나고 나면 서비스의 종료 절차를 진행하는 스레드만이 서비스에 대한 모든 상태를 사용할 수 있도록 정리할 수 있다.
즉 코드 가운데 크리티컬 섹션에 다른 스레드가 들어오지 못하도록 하기 위해 락을 사용하는 대신
이와 같이 스레드 간의 약속을 정해 다른 스레드가 작업을 방해하지 않도록 하는 방법이 있다는 점을 알아두자.



* 10.1.5. 리소스 데드락

-
필요한 자원을 사용하기 위해 대기하는 과정에도 데드락이 발생할 수 있다.
( 스레드 A B 가 각각 DB A 와 DB B 를 들고 있는 상황에서 DB B 와 DB A 를 요청하는 경우 )


-
자원과 관련해 발생할 수 있는 또 다른 데드락 상황은 스레드 부족 데드락이다.
단일 스레드로 동작하는 Executor 에서 현재 실행 중인 작업이 또 다른 작업을 큐에 쌓고는 그 작업이 끝날 때까지 대기하는 데드락 상황이 대표적이다.





반응형

댓글