gimmesilver's blog

Agbird.egloos.com

포토로그



자바 병행 프로그래밍시 유의해야 하는 버그 패턴 6가지 프로그래밍

원문: http://www.ibm.com/developerworks/java/library/j-concurrencybugpatterns/index.html?ca=drs-

6줄 요약:
1. 비원자적 연산에 volatile 변수를 사용하지 말아라
2. 가변 필드는 동기화 객체로 사용하지 말아라.
3. java.util.concurrent.Lock 사용할 때는 반드시 finally 구문에서 락을 해제하는 코드를 작성해라.
4. 성능을 위해 동기화 블록 범위를 최소화해라.
5. 여러 단계로 리소스에 접근해야 하는 경우 전체 단계를 하나의 동기화 블록에 포함시켜라.
6. 중첩 동기화 블록을 작성할 때는 동기화 순서를 엄밀하게 정의해라.

 위 글에 몇 가지 첨언하자면, 
 우선 volatile 키워드는 가급적 사용하지 않는 것이 좋다. 자바에서 volatile 을 써야 할만큼 최적화가 필요한 경우는 거의 없다고 봐도 되며 대개의 경우 Atomic 클래스를 사용하는 것이 성능을 떨어뜨리지 않으면서도 가독성이나 유연성 면에서 훨씬 낫다.

 2번과 관련해서 예전에 비슷한 글(http://agbird.egloos.com/4863601)을 쓴적이 있는데 참조 변수와 실제 객체의 차이를 명확히 인식하고 있지않으면 나중에 검출하기 힘든 미묘한 버그를 만들 수 있다. 동기화 대상은 어디까지나 참조 변수가 아니라 해당 변수가 가리키고 있는 실제 객체이다.

 4번에 나오는 원문의 예제는 주의깊게 볼 필요가 있는 코드이다. 원래 코드(Listing 5)는 다음과 같다.

public class Operator {
   private int generation = 0; //shared variable
   private float totalAmount = 0; //shared variable
   private final Object lock = new Object();
   public void workOn(List<Operand> operands) {
      synchronized (lock) {
         int curGeneration = generation; //requires synch
         float amountForThisWork = 0;
         for (Operand o : operands) {
            o.setGeneration(curGeneration);
            amountForThisWork += o.amount;
         }
         totalAmount += amountForThisWork; //requires synch
         generation++; //requires synch
      }
   }
}

그리고 원문에서 제시한 수정된 코드(Listing 6)는 아래와 같다. 

public void workOn(List<Operand> operands) {
   int curGeneration;
   float amountForThisWork = 0;
   synchronized (lock) {   // 1)
      int curGeneration = generation++;
   }
   for (Operand o : operands) {  // 2)
      o.setGeneration(curGeneration);
      amountForThisWork += o.amount;
   }
   synchronized (lock) {   // 3)
      totalAmount += amountForThisWork;
   }
}

 만약 A 라는 스레드에서 1)을 수행한 후 2)를 수행하는 도중에 B 라는 스레드가 1),2),3)을 모두 수행한 다음에 A 스레드가 다시 3)을 수행하는 경우를 생각해 보자. 이 경우 generation 변수는 B 스레드 수행에 의한 결과를 갖고 있지만 totalAmount 는 A 스레드 수행에 의한 결과값을 갖게 된다. 따라서 generation 과 totalAmount 변수 사이의 의미적인 의존성이 깨지고 만다. 

 이런 문제를 해결하기 위해 원래 코드(Listing 5)를 아래와 같이 바꿀 수 있다.

public void workOn(List<Operand> operands) {
   int curGeneration;
   float amountForThisWork = 0;
   synchronized (lock) {
      curGeneration = generation;
   }
   for (Operand o : operands) {
      o.setGeneration(curGeneration);
      amountForThisWork += o.amount;
   }
   synchronized (lock) {
      totalAmount += amountForThisWork;
      generation++;
   }
}

 위와 같이 동기화 블록을 나눈다면 totalAmount 와 generation 은 동일한 동기화 블록에서 수정되기 때문에 Listing 6 에서의 문제는 발생하지 않는다. 하지만 이 경우에는 여러 스레드에서 중복된 계산 즉, 동일한 generation 값을 가지고 totalAmount 값을 계산하는 경우가 발생할 것이다. 

 결국 위 두 경우 모두 Listing 5 의 의도를 충실히 반영하는 코드가 아니다. 따라서 이 경우에는 동기화 블록을 쪼개지 않는 것이 좋다. 만약 성능을 위해 반드시 for 문을 동기화 블록 밖으로 빼야 한다면 generation 변수와 totalAmount 변수의 쓰임새를 정확히 따져보고 위 두 방법 중 어느 것을 택할지 결정해야 한다.
 

덧글

댓글 입력 영역