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

프로그래밍/병렬프로그래밍 2010. 4. 8. 17:07


 개인적인 스터디를 위해 위 책의 내용을 정리한 내용입니다. 자세한 내용을 확인하고 싶으신 분은 위 책을 구매하셔서 확인하시기 바랍니다. http://www.yes24.com/24/goods/3015162?CategoryNumber=001001003

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

                                }

                     }

           }                   

}

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;

           }

}

 

10.1.4 오픈 호출 open call

오픈 호출은 락을 전혀 확보하지 않은 상태에서 메소드를 호출하는 것을 말한다.

public void setLocation(Point location){

           boolean reachedDestination;

           sychronized(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.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 상태는 스레드가 작업을 진행하는데 꼭 필요한 자원을 영영 할당받지 못하는 경우에 발생한다.

- 소모 상황이 발생하는 원인은 대부분 스레드의 우선 순위 priority 를 적절치 못하게 올리거나 내리는 부분에 있다. 또한 락을 확보한 채로 종료되지 않는 코드.

- 스레드 우선 순위를 변경하고 나면 플랫폼에 종속적인 부분이 많아지며, 따라서 활동성 문제를 일으키기 쉽다. 대부분의 병렬 애플리케이션은 모든 스레드의 우선 순위에 기본값을 사용하고 있다.

 

10.3.2 형편 없는 응답성

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

특정 스레드가 대량의 데이터를 넘겨 받아 처리하느라 필요 이상으로 긴 시간 동안 락을 확보하고 있다면 넘겨준 대량의 데이터를 사용해야 하는 다른 스레드는 데이터를 받아올 때까지 상당히 긴 시간동안 대기해야 한다.

 

10.3.3 라이브락

라이브락은 에러를 너무 완벽하게 처리하고자 회복 불가능한 오류를 회복 가능하다고 판단해 계속해서 재시도하는 과정에 나타난다.

: