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

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);
            }
        }
    }
}

transferMoney(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 taxis;
    
    @GuardedBy("this")
    private final Set availableTaxis;

    public Dispatcher() {
        taxis = new HashSet();
        availableTaxis = new HashSet();
    }

    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 copy;
    
    sychronized(this) {
        copy = new HashSet(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 라이브락

  • 무한 반복하는 경우를 말함
  • 라이브락은 에러를 너무 완벽하게 처리하고자 회복 불가능한 오류를 회복 가능하다고 판단해 계속해서 재시도하는 과정에 나타난다.
  • 라이브락 해결을 위해 재시도 시 약간의 변형을 넣는 방법이 있음 (예, 이더넷 프로토콜에서 신호 충돌 시 임의의 시간 대기 후 재시도)