* 영어 연습 삼아서 한 발번역이라 의미의 왜곡이 있을 수 있으니 반드시 원문을 참고 삼으시길 바랍니다.
Java concurrency bug patterns for multicore systems
http://www.ibm.com/developerworks/java/library/j-concurrencybugpatterns/index.html?ca=drs-
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 필드에 행해진 연산.
- 첫째로 _set 이 volatile로 선언되어있는데, 여러개의 스레드가 이 필드에 접속할 수 있다는 의미가 담겨있다.
- 그러나 _set++ 은 최소 단위 연산(atomic)이 아니다. 즉, 반드시 단 하나의 연산으로 끊김없이 실행되지 않는다는 뜻이다. 이는 읽고-수정하고-쓰는(read-modify-write) 세개의 독립적인 연산들을 짧게 표현한 것이다.
- 끝으로, _set++는 락으로 보호되지 않는다. 여러개의 스레드가 메소드 register() 를 동시에 호출하면 경쟁 상황(race condition)이 발생해서 _set 값이 부적절하게 설정된다.
이러한 유형의 에러는 Jetty의 사례에서처럼 여러분의 코드에서도 쉽게 나타날 수 있다. 이제 에러가 어떻게 발생하는지 자세히 들여다 보자.
버그 패턴의 요소들
코드의 논리적 순서를 따라가보면 이런 버그 패턴을 분명하게 드러내는데 도움이 된다.
멀티 스레드에 안전한(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 톰켓의 에러
그림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. 락 누수가 발생하는 구조
Listing 4. 항상 unlock() 호출을 finally 블록 안에 넣을 것.
4. Performance tuning synchronized blocks
어떤 병렬 프로그래밍 버그는 코드를 망치지는 않겠지만 애플리케이션의 성능을 떨어뜨릴 수 있다. Listing 5의 synchronized 블록을 보자.
Listing 5. Synchronized block invariant code
Listing 6. Synchronized block without the invariant code
What about ....
아마 두 공유 변수를 AtomicInteger와 AtomicFloat로 사용해서 동기화 블록을 모두 없애는게 더 좋지 않을까 생각할 것이다. 이것이 가능한지는 다른 메서드들이 이 변수들을 이용해서 무엇을 하는지, 그리고 그들 사이에 의존성이 있는지에 달려있다.
아마 두 공유 변수를 AtomicInteger와 AtomicFloat로 사용해서 동기화 블록을 모두 없애는게 더 좋지 않을까 생각할 것이다. 이것이 가능한지는 다른 메서드들이 이 변수들을 이용해서 무엇을 하는지, 그리고 그들 사이에 의존성이 있는지에 달려있다.
5. 다단계 접속
여 러분이 두 개의 테이블을 갖는 애플리케이션을 만들고 있다고 하자. 한 테이블은 직원 이름을 사원 번호와 연결하고 다른 테이블은 사원 번호를 급여와 연결하고 있다. 이 데이타는 동시에 접속하고 수정되어야 할 필요가 있고, 이것은 Listing 7에서 보듯이 스레스 안정적인 ConcurrentHashMap 을 통해서 가능하다.
Listing7. 2단계 접속
각각의 map 객체 자체가 스레드 안정적인것만으로는 충분치 않다. 두 map 객체 사이에는 의존성이 있어서 양쪽을 접속하는 일부 연산들은 최소 단위 접속atomic access을 필요로 한다. 이런 사례에서는 java.util.HashMap같이 스레드 안정적이지 않은 컨테이너를 사용하고 각각의 접속을 보호하기 위해서 명시적인 동기화를 적용함으로써 스레드 안정성을 이룰 수 있다. 필요하다면 동기화 구문은 두 map 객체의 접속을 모두 포함할 수 있다.
6. 대칭적 데드락
클라이언트 프로그램에게 스레스 안정성을 보장하는 자료구조를 나타내는 컨테이너 클래스를 떠올려보자.(클라이언트가 사용하는 코드 주변을 동기화해야하는 java.util 패키지내에 들어있는 대부분의 컨테이너들과는 사뭇 다르다.) Listing 8에서 참조가 변경 가능한 멤버 변수가 데이터를 저장하고 락 객체가 멤버 변수에 대한 모든 접속을 보호한다.
Listing 8. 스레드 안정적인 컨테이너
Listing 9. 데드락을 유발하는 구조
두 인스턴스의 락들을 함께 취해야 할 때 락을 취하는 순서가 자동으로 계산되어서 어떤 락을 먼저 취할 것인지 결정되도록 인스턴스 사이의 순서를 결정함으로써 대칭적 데드락을 방지할 수 있다. 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 |