* 영어 연습 삼아서 한 발번역이라 의미의 왜곡이 있을 수 있으니 반드시 원문을 참고 삼으시길 바랍니다.
Java concurrency bug patterns for multicore systems
http://www.ibm.com/developerworks/java/library/j-concurrencybugpatterns/index.html?ca=drs-


1. Jetty에 담겨있는 안좋은 패턴(antipattern)

첫번째  버그는 널리 사용되는 오픈소스 HTTP 서버인 Jetty 에서 발견된다. 이것은 Jetty 커뮤니티에서 확인된 실제 버그이다.

Listing 1. 락을 확보하지 않고 volatile 필드에 행해진 연산.
Listing 1에 나타난 에러는 is the sum of its parts:

  • 첫째로 _set 이 volatile로 선언되어있는데, 여러개의 스레드가 이 필드에 접속할 수 있다는 의미가 담겨있다.
  • 그러나 _set++ 은 최소 단위 연산(atomic)이 아니다. 즉, 반드시 단 하나의 연산으로 끊김없이 실행되지 않는다는 뜻이다. 이는 읽고-수정하고-쓰는(read-modify-write)  세개의 독립적인 연산들을 짧게 표현한 것이다.
  • 끝으로, _set++는 락으로 보호되지 않는다. 여러개의 스레드가 메소드 register() 를 동시에 호출하면 경쟁 상황(race condition)이 발생해서 _set 값이 부적절하게 설정된다.


이러한 유형의 에러는 Jetty의 사례에서처럼 여러분의 코드에서도 쉽게 나타날 수 있다. 이제 에러가 어떻게 발생하는지 자세히 들여다 보자.

버그 패턴의 요소들

코드의 논리적 순서를 따라가보면 이런 버그 패턴을 분명하게 드러내는데 도움이 된다.
변수 i에 대한 위와 같은 연산들은 최소 단위 실행이 아니다.(즉, read-modify-write처럼 여러개의 연산임.) 자바 언어에서 volatile 키워드는 오직 변수의 가시성만을 보장할 뿐 연산의 최소단위성(atomicity)은 보장하지 않음을 안다면 여기서 잠시 작업을 멈출 것이다. volatile 필드에 대해서 락으로 감싸지 않은 non-atomic 연산이 여러개의 스레드에 의해서 동시에 실행되면 경쟁 상태(race condition)을 유발할 가능성이 높다.

멀티 스레드에 안전한(thread-safe) 프로그램에서 변수를 volatile로 선언함으로써 오직 하나의 쓰기 스레드만이 변수값을 수정할 수 있고, 나머지 스레드들은 최신의 값을 읽을 수 있다.

따라서 코드에 버그가 있는지는 얼마나 많은 스레드가 동시에 연산을 실행할 수 있는가에 달려있다. start-join 관계나 외부 락설정으로 오직 하나의 스레드가 non-atomic 연산을 호출한다면 코드는 멀티 스레드 환경에서 안전할 것이다.

자바 코드에서 volatile 키워드는 오직 변수의 "가시성"을 보장함을 기억해야 한다. volatile 키워드는 연산의 최소 단위성(atomicity)을 보장하지는 않는다. 변수에 대한 연산이 최소 단위 연산이 아니고(non-atomic) 여러개의 스레드가 접속하는 경우, volatile을 이용한 동기화 방법에 의존해서는 안된다. 그 대신에 동기화 블록이나 lock 클래스, 그리고 java.util.concurrent 패키지 내에 들어있는 atomic class들 을 사용하자. 이것들은 프로그램의 스레스 안전성을 확실히 하기 위해서 설계된 것들이다.

2. 참조가 변하는 필드들에 대한 동기화

자바 언어에서 상호배제 락을 확보하기 위해서 동기화 블록을 사용하는데, 이를 통해서 멀티 스레드 시스템에서 공유 자원에 대한 접속을 보호한다. 그러나 참조가 변하는 필드를 동기화할 때 상호 배제가 깨질 수 있는 허점이 있다. 해결책은 동기화될 필드를 항상 private final 로 선언하는 것이다. 이를 이해하기 위해서 문제를 좀 더 자세히 살펴보자.

갱신되는 필드들을 동시에 락걸기

동기화 블록은 동기화 되는 필드 자체 보다는 필드가 참조하는 객체를 통해서 보호된다. 동기화 필드의 참조가 변경 가능하다면(필드가 초기화 된 이후에 프로그램의 어느 곳에서든 다른 참조를 부여받을 수 있다는 뜻?), 다른 스레드들이 서로 다른 객체에 대해서 동기화 될 수 있기 때문에, 유용한 의미가 없을 것이다.

이 문제를 Listing 2에서 볼 수 있는데 톰켓에서 가져온 코드 조각이다.

Listing 2 톰켓의 에러
listeners 변수가 배열 A를 참조한다고 하고, 스레드 T1이 배열 A에 대해 락을 확보하고나서 배열B(위에서 지역 변수 results 가 참조하는 배열 객체)를 생성하느라 바쁜 상태(for문 실행중)라고 가정하자. 그러는동안, T2가 뒤따라와서 배열 A에 대한 락을 확보하려고 블록 상태로 들어간다. T1이 변수 listeners 의 참조를 배열 B로 변경한 후 블록을 빠져나갈 때 T2 가 배열 A의 락을 확보하고 배열 B의 복사본을 만들기 시작한다. 이 때 스레드 T3가 뒤따라와서 배열 B의 락을 확보한다(T1이 변수 listeners의 참조를 배열 B로 바꾸었으니까). T2와 T3가 서로 다른 락을 획득했기 때문에 이 두 스레드는 이제 동시에 배열 B의 복사본을 갖게 된다.

그림1이 이를 보여주고 있다.

[그림1] 참조가 변하는 변수에 대한 동기화로 상호 배제가 안됨.


수많은 바람직하지 않은 행위들이 이와 같은 참조값 설정에서 일어날 수 있다. 최소한 새로운 listeners 중 적어도 하나는 소실되거나 스레드들 중 하나는 ArrayIndexOutOtBoundsException 을 받게 될 것이다. (listeners 참조와 배열 길이가 메소드 내의 어느 지점에서든 변할 수 있기 때문)

항상 동기화 필드를 private final 로 설정해서 락 객체가 바뀌지 않고 mutex가 보장되도록 하는 것이 좋은 습관이다.

3. java.util.concurrent lock leak

java.util.concurrent.locks.Lock 인터페이스를 구현하는 락은 여러개의 스레드가 공유 자원에 접속하는 방법을 제어한다. 이러한 락들은 블록 구문을 사용하지 않아서 동기화 메소드나 구문보다 유연하다. 그러나 블록 구문이 없어도 되는 락은 결코 자동으로 해제되지 않기 때문에 이러한 유연함이 코딩 에러를 유발할 수 있다. 만일 동일한 인스턴스상에서 Lock.lock() 호출이 대응하는 unlock() 호출을 갖지 못하면 락 누수(lock leak)로 이어진다.

예외를 던지는 것처럼 동기화되는 코드내의 메소드 행위를 살펴봄으로써 java.util.concurrent 락 누수 버그를 쉽게 만들어낼 수 있다. Listing 3에서 볼 수 있듯이 accessResource 메소드는 공유 자원에 접속하는동안 InterruptedException을 던질 수 있다. 그 결과로 unlock() 은 호출되지 않는다.

Listing 3. 락 누수가 발생하는 구조
락 해제를 보장하려면 단순히 모든 lock() 메소드를 unlock() 메소드와 같이 붙어다니게 하면 되는데, 두 메소드를 try-finally 블록에 위치시켜야 한다. 이것은 Listing 4에 잘 나와있다.

Listing 4. 항상 unlock() 호출을 finally 블록 안에 넣을 것.

4. Performance tuning synchronized blocks

어떤 병렬 프로그래밍 버그는 코드를 망치지는 않겠지만 애플리케이션의 성능을 떨어뜨릴 수 있다. Listing 5의 synchronized 블록을 보자.

Listing 5. Synchronized block invariant code
Listing5 에서 두 개의 공유 변수에 대한 접속은 완벽하게 동기화되지만, 자세히 보면 synchronized 블록 안에서 필요 이상으로 많은 연산이 이루어짐을 알 것이다. 코드를 재배치함으로써 Listing 6에 보이듯이 이를 바로잡을 수 있다.

Listing 6. Synchronized block without the invariant code
두 번째 버전의 코드는 멀티코어(multicore) 머신에서 더 좋은 성능을 낼 것이다. 그 이유는 Listing 5에서 동기화 블록이 병렬 실행을 방해하기 때문이다. 위의 메소드에서는 반복문에서 계산 시간이 소모될 것으로 보인다. 보통의 경우, 될 수 있으면 스레드 안전성을 해치지 않으면서 동기화 블록을 간결하게 만들도록 노력하자.

What about ....
아마 두 공유 변수를 AtomicInteger와 AtomicFloat로 사용해서 동기화 블록을 모두 없애는게 더 좋지 않을까 생각할 것이다. 이것이 가능한지는 다른 메서드들이 이 변수들을 이용해서 무엇을 하는지, 그리고 그들 사이에 의존성이 있는지에 달려있다.

5. 다단계 접속

여 러분이 두 개의 테이블을 갖는 애플리케이션을 만들고 있다고 하자. 한 테이블은 직원 이름을 사원 번호와 연결하고 다른 테이블은 사원 번호를 급여와 연결하고 있다. 이 데이타는 동시에 접속하고 수정되어야 할 필요가 있고, 이것은 Listing 7에서 보듯이 스레스 안정적인 ConcurrentHashMap 을 통해서 가능하다.

Listing7. 2단계 접속
위 방식이 스레드 안정적인듯 하나, 사실은 그렇지 않다. 문제는 getBonusFor 메소드가 스레드 안정적이지 않다는 데에 있다. 사원 번호를 얻어내는 지점과 이 번호를 이용해서 급여를 얻어내는 지점 사이에 또다른 스레드가 양쪽 테이블에서 작업 중인 직원을 삭제할 수 있다. 그렇게 되면 두번째 map 객체에 접속할때 null이 반환되고 예외가 발생한다.

각각의 map 객체 자체가 스레드 안정적인것만으로는 충분치 않다. 두 map 객체 사이에는 의존성이 있어서 양쪽을 접속하는 일부 연산들은 최소 단위 접속atomic access을 필요로 한다. 이런 사례에서는 java.util.HashMap같이 스레드 안정적이지 않은 컨테이너를 사용하고 각각의 접속을 보호하기 위해서 명시적인 동기화를 적용함으로써 스레드 안정성을 이룰 수 있다. 필요하다면 동기화 구문은 두 map 객체의 접속을 모두 포함할 수 있다.

6. 대칭적 데드락

클라이언트 프로그램에게 스레스 안정성을 보장하는 자료구조를 나타내는 컨테이너 클래스를 떠올려보자.(클라이언트가 사용하는 코드 주변을 동기화해야하는 java.util 패키지내에 들어있는 대부분의 컨테이너들과는 사뭇 다르다.) Listing 8에서 참조가 변경 가능한 멤버 변수가 데이터를 저장하고 락 객체가 멤버 변수에 대한 모든 접속을 보호한다.

Listing 8. 스레드 안정적인 컨테이너
이제 또다른 ConcurrentHeap 인스턴스를 파라미터로 취해서 그 안의 모든 원소들을 현재의 인스턴스에 추가하는 메소드를 만들자. 이 메소드는 두 인스턴스의 elements 멤버 변수를 모두 접속할 필요가 있으니 Listing 9 에서 보이듯이 두개의 락을 취한다.

Listing 9. 데드락을 유발하는 구조
데드락이 발생할 가능성이 눈에 보이는가? 한 프로그램이 두 개의 인스턴스 heap1과 heap2를 갖고 있다고 가정하자. 한 스레드가 heap1.addAll(heap2)를 호출하고 또다른 스레드가 동시에 heap2.addAll(heap1)을 호출하면 두 스레드는 데드락으로 끝날 수 있다. 다른 말로 해보면, 첫 번째 스레드가 heap2의 락을 취하는데 그러기 전에 두 번째 스레드 또한 heap1의 락을 취하면서 메소드를 실행한다고 하자. 결과적으로 각각의 스레드는 상대방 스레드가 취한 락을 기다리는 걸로 멈추게 된다.

두 인스턴스의 락들을 함께 취해야 할 때 락을 취하는 순서가 자동으로 계산되어서 어떤 락을 먼저 취할 것인지 결정되도록 인스턴스 사이의 순서를 결정함으로써 대칭적 데드락을 방지할 수 있다.  Brian Goetz는 그의 책인 Java Concurrency in Practice에서 회피 방법을 논의하고 있다.

'Dev > Java' 카테고리의 다른 글

[JSR 310] New Date and Time API  (0) 2012.09.20
[Swing JTable] JTable 다루기 2  (7) 2010.06.01
[Swing JTable] JTable 다루기 1  (3) 2010.05.30
Posted by yeori
,