10.1 데드락
자바 프로그램에서 데드락이 발생하면 프로그램을 강제로 종료하기 전에는 영원히 멈춘 상태로 유지된다.
10.1.1 락 순서에 의한 데드락
예제 코드는 데드락이 발생할 여지가 있다.
- public class LeftRightDeadlock {
- private final Object left = new Object();
- private final Object right = new Object();
- public void leftRight() {
- synchronized(left) {
- synchronized(right) {
- doSomething();
- }
- }
- }
- public void rightLeft() {
- synchronized(right){
- synchronized(left) {
- doSomething();
- }
- }
- }
- }
프로그램 내부의 모든 스레드에서 필요한 락을 모두 같은 순서로만 사용한다면 락 순서에 의한 데드락은 발생하지 않는다.
10.1.2 동적인 락 순서에 의한 데드락
- public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount)
- throws InsufficientFundsException {
- synchronized(fromAccount) {
- synchronized(toAccount) {
- if (fromAccount.getBalance().compareTo(amount) < 0)
- throw new InsufficientFundsException();
- else {
- fromAccount.debit(amount);
- toAccount.credit(amount);
- }
- }
- }
- }
transfreMoney(myAccount, yourAccount, 10);
transferMoney(yourAccount, myAccount, 20);
위 처럼 호출시 데드락에 빠질 여지가 있다.
락의 순서를 프로그램으로 제어할 수 있다면 데드락을 방지할 수 있다.
System.identityHashCode 메소드를 사용하여 객체의 순서를 정의해 락을 걸어주면 데드락을 방지할 수 있다. 만일에라도 객체의 순서가 같을 경우를 대비해 tieLock 을 사용한다.
- private static final Object tieLock = new Object();
- public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount)
- throws InsufficientFundsException {
- class Helper {
- public void transfer() throws InsufficientFundsException {
- if (fromAcct.getBalance().compareTo(amount) < 0)
- throw new InsufficientFundsException();
- else {
- fromAcct.debit(amount);
- toAcct.credit(amount);
- }
- }
- }
- int fromHash = System.identityHashCode(fromAcct);
- int toHash = System.identityHashCode(toAcct);
- if(fromHash < toHash) {
- synchronized(fromHash) {
- synchronized(toAcct) {
- new Helper().transfer();
- }
- }
- } else if (fromHash > toHash) {
- synchronized(toAcct) {
- synchronized(fromAcct) {
- new Helper().transfer();
- }
- }
- } else {
- synchronized(tieLock) {
- synchronized(fromAcct) {
- synchronized(toAcct) {
- new Helper.transfer();
- }
- }
- }
- }
- }
10.1.3 객체 간의 데드락
Taxi 의 setLocation 메소드에서는 Taxi, Dispatcher 의 순서로 락을 획득하며
Dispatcher 의 getImage 메소드에서는 Dispatcher, Taxi 의 순서로 락을 획득한다.
따라서 데드락에 빠질 위험이 있다.
- class Taxi{
- @GuardedBy("this") private Point location, destination;
- private final Dispatcher dispatcher;
- public Taxi(Dispatcher dispatcher){
- this.dispatcher = dispatcher;
- }
- public synchronized Point getLocation(){
- return location;
- }
- public synchronized void setLocation(Point location) {
- this.location = location;
- if (location.equals(destination))
- dipatcher.notifyAvailable(this);
- }
- }
- class Dispatcher {
- @GuardedBy("this") private final Set<Taxi> taxis;
- @GuardedBy("this") private final Set<Taxi> availableTaxis;
- public Dispatcher(){
- taxis = new HashSet<Taxi>();
- availableTaxis = new HashSet<Taxi>();
- }
- public synchronized void notifyAvailable(Taxi taxi) {
- availableTaxis.add(taxi);
- }
- public synchronized Image getImage() {
- Image image = new Image();
- for (Taxi t : taxis)
- image.drawMarker(t.getLocation());
- return image;
- }
- }
- public void setLocation(Point location) {
- boolean reachedDestination;
- synchronized (this) {
- this.location = location;
- reachedDestination = location.equals(destination);
- }
- if( reachedDestination)
- dipatcher.notifyAvailable(this);
- }
- public Image getImage() {
- Set<Taxi> copy;
- sychronized (this) {
- copy = new HashSet<Taxi>(taxis);
- }
- Image image = new Image();
- for (Taxi t : copy)
- image.drawMarker(t.getLocation());
- return image;
- }
10.1.4 오픈 호출 open call
- 오픈 호출은 락을 전혀 확보하지 않은 상태에서 메소드를 호출하는 것을 말한다.
- 메소드 호출이 모두 오픈 호출로 이루어진 클래스는 락을 확보한 채 메소드 호출하는 클래스 보다 안전, 분석 작업이 간편함
10.1.5 리소스 데드락
두 개의 데이터 베이스에 대한 커넥션 풀을 사용할 때 두개의 커넥션을 모두 요청하는 경우
A 스레드는 D1, D2 의 순서로 커넥션을 요청하고 B 스레드는 D2, D1 의 순서로 요청하는 경우
커넥션풀의 크기가 클수록 문제가 발생할 확률은 줄어들지만 발생할 여지는 존재한다.
10.2 데드락 방지 및 원인 추적
한번에 하나 이상의 락을 사용하지 않는 프로그램은 락의 순서에 의한 데드락이 발생하지 않는다.
10.2.1 락의 시간 제한
- 암묵적인 락은 락을 확보할 때까지 영원히 기다림
- Lock 클래스의 메소드 가운데 시간을 제한할 수 있는 tryLock 메소드를 사용하면 지정한 시간 또한 락을 확보하지 못한다면 tryLock 메소드가 오류를 발생시키도록 할 수 있다.
10.2.2 스레드 덤프를 활용한 데드락 분석
스레드 덤프에는 실행 중인 모든 스레드의 스택 트레이스 stack trace 가 담겨 있다. 락과 관련된 정보 또한 담겨 있다.
JVM이 스레드 덤프를 생성하도록 하려면 Unix 플랫폼에서는 JVM 프로세스에 SIGQUIT 시그널(kill -3)을 전송하거나 Ctrl-\ 키를 누르면 되고, 윈도우 환경에서는 Ctrl-Break 키를 누르면 된다.
10.3 그 밖의 활동성 문제
활동성을 떨어뜨리는 주된 원인은 데드락이지만, 소모 starvation, 놓친 신호, 라이브락, livelock 같은 다양한 원인이 존재한다.
10.3.1 소모
- 소모 starvation 상태는 스레드가 작업을 진행하는데 꼭 필요한 자원(주로 CPU)을 영영 할당 받지 못하는 경우에 발생한다.
- 소모 상황이 발생하는 원인은 대부분 스레드의 우선 순위 priority 를 적절치 못하게 올리거나 내리는 부분에 있다. 또는 락을 확보한 채로 종료되지 않는 코드.
- 스레드 우선 순위를 변경하고 나면 플랫폼에 종속적인 부분이 많아지며, 따라서 활동성 문제를 일으키기 쉽다.
- 일반적인 상황에서는 우선 순위를 변경하지 않는 것이 현명함.
10.3.2 형편 없는 응답성
- 애플리케이션의 응답성이 떨어진다면 락을 제대로 관리하지 못하는 것이 원인일 수 있다.
- 특정 스레드가 대량의 데이터를 넘겨 받아 처리하느라 필요 이상으로 긴 시간 동안 락을 확보하고 있다면 넘겨준 대량의 데이터를 사용해야 하는 다른 스레드는 데이터를 받아올 때까지 상당히 긴 시간 동안 대기해야 한다.
10.3.3 라이브락
- 무한 반복하는 경우를 말함
- 라이브락은 에러를 너무 완벽하게 처리하고자 회복 불가능한 오류를 회복 가능하다고 판단해 계속해서 재시도하는 과정에 나타난다.
- 라이브락 해결을 위해 재시도 시 약간의 변형을 넣는 방법이 있음.
(예, 이더넷 프로토콜에서 신호 충돌 시 임의의 시간 대기 후 재시도)
'개발 > 병렬 프로그래밍' 카테고리의 다른 글
자바 병렬 프로그래밍 - 13장 명시적인 락 (0) | 2016.09.07 |
---|---|
자바 병렬 프로그래밍 - 11장 성능, 확장성 (0) | 2016.09.04 |
자바 병렬 프로그래밍 - 8장 스레드 풀 활용 (0) | 2016.08.30 |
자바 병렬 프로그래밍 - 7장 중단 및 종료 (0) | 2016.08.29 |
자바 병렬 프로그래밍 - 6장 작업실행 (0) | 2016.08.16 |