태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

[ Enterprise Java는 거대한 동기화 머신이다 - Thread ] Enterprise Java & Oracle 성능 분석의 기초 - Part2

Enterprise Java 2007.08.26 00:44
Enterprise Java는 거대한 동기화 머신이다.

작년 5월 쯤 [오라클은 거대한 동기화 머신이다]라는 타이틀로 오라클 성능 분석에 대한 책을 낸 적이 있다.

OWI Advanced Oracle Wait Interface in 10g

조동욱

엑셈 2006.05.08



(책 광고 아님)

이 책에서 이야기하고자 핵심은 다음과 같은 것이다.
  • 오라클에서는 동시에 많은 수의 세션이 동일한 자원(Library Cache/Buffer Cache/Table/Row/...)을 획득하기 위해 경쟁한다.
  • 여러 세션이 동시에 자원을 획득하고 변경하는 것을 방지하기 위해 동기화 메커니즘이 필요하다.
  • 오라클에서는 Lock과 Latch라는 개념을 통해 동기화 메커니즘을 제공한다.
  • Lock과 Latch을 통한 동기화 과정에서 많은 성능 문제가 발생하며 이를 대기이벤트(Wait Event)로 관찰하고 분석할 수 있다.
이 책에서 주장했던 [동기화 메커니즘]은 동시 세션/사용자/쓰레드를 지원하는 시스템에서는 공통적으로 사용된다. Enterprise Java를 대표하는 WAS 환경도 예외가 아니다.

WAS Application에서는 수십 개 ~ 수백 개의 쓰레드가 동일한 자원을 획득하기 위해 경쟁한다. 이 과정에서 필연적으로 동기화 문제가 발생하며 이로 인한 대기 현상(Wait)가 발생하게 된다.

한가지 안타까운 것은 Java 환경에서는 동기화(Synchronization)에 의한 대기 현상과 성능 저하 현상을 관찰하는 체계적인 방법이 없다는 것이다. 오라클에서는 V$SESSION, V$SESSION_WAIT, V$SESSION_EVENT, V$SYSTEM_EVENT, V$LOCK, V$LATCH, ... 등 수많은 뷰를 통해서 Lock과 Latch 등 동기화 객체의 의한 대기 현상을 효과적으로 분석할 수 있다.

반면에 Java 환경에서는 몇가지 툴들이 흩어져서 제공되며, 그나마 오라클이 제공하는 정보에 비하면 상당히 체계가 부족하다. 하지만 오라클은 "Application이고 System인 반면", Java는 "Language이고 Library"이라는 점에서 보면 당연한 결과라고 할 수 있겠다.
(물론 JMX와 같은 표준들이 나오면서 Java 환경도 점점 성능 관리에 대한 개념과 기능들이 추가되고 있다)

Java의 동기화(Synchronization) 방법
(이미 널리 알려진 진부한 사실들이지만 간단히 정리해볼 필요는 있어서...)

Monitor
Java는 기본적으로 Multi Thread 환경을 전제로 설계되었다. 따라서 Multi Thread 환경에서 발생할 수 있는 동기화 문제를 해결하기 위한 기본적인 메커니즘을 제공한다.

이것을 흔히 "Monitor"라고 부른다. Java에서의 모든 Object는 반드시 Monitor를 하나씩 가지고 있다. 특정 Object의 Monitor에는 동시에 하나의 Thread만이 들어갈 수(Enter) 있다. 다른 Thread에 의해 이미 점유된 Monitor에 들어가고자 하는 Thread는 Monitor의 Wait Set에서 대기해야만 한다.

Java에서 Monitory를 점유하는 유일한 방법은 synchronized 키워드를 사용하는 것이다. 가령 다음과 같이 ...

synchronized(dump_test.lock) {
for(int idx=0; idx
}

위의 코드를 실행하는 Thread는 for(...) {} 구문이 실행되는 동안 dump_test.lock 이라는 Object의 Monitor를 점유한다. 따라서 dump_test.lock Object에 대해 Monitor를 점유하려는 모든 Thread는 for(...) {} 구문이 실행되는 동안 대기 상태(Blocked)에 빠지게 된다.

위의 Java 코드에 해당하는 Byte Code를 보면 흥미로운 사실을 알 수 있다.

GETSTATIC dump_test.lock : Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER
...

ALOAD 1
MONITOREXIT

synchronized { } 블록은 MONITORENTER ... MONITOREXIT Byte Code로 해석된다. 즉 Monitor에 들어간 후 원하는 코드를 실행하고 다시 Monitor를 빠져 나오는 것이 Java가 동기화를 수행하는 방법이다.

동기화 프로그래밍 기법
익히 알려진 대로 Java에서 동기화를 수행하는 방법은 크게 두 가지로 나뉜다.

  1. synchronization: synchronized 블록이나 synchronized 메소드를 통해 동기화를 수행하는 방식
  2. wait/notify: wait와 notify 메소드를 이용해서 동기화를 수행하는 방식. 기본적으로 synchronized 방식의 "응용"이라고 봐도 무방하다.
개인적으로는 두 방법 다 매우 직관적이고 사용하기 편하다고 생각한다. 하지만 많은 사람들이 두 방법에 대해 오해를 하거나 잘 모르는 경우가 많다.

가령 여러 Thread가 동시에 Access할 수 있는 객체는 무조건 sychronized 블록이나 메소드로 보호를 해야 하는가?
No. synchronized를 수행하는 코드와 그렇지 않은 코드의 성능 차이는 대단히 크다. 동기화를 위해 Monitor에 액세스하는 작업에는 그에 상응하는 오버헤드가 따른다. 따라서 반드시 필요한 경우에만 사용해야 한다. 가령 다음과 같은 코드를 보자.

private static instance = null;

public static synchronized getInstance() {
if(instance == null) { instance = new Instance(...); }
return instance;
}

Singleton 방식을 구현하기 위해 getInstance 메소드를 Synchronized로 잘 보호했지만, 성능에 있어서는 불행한 결정이 될 수 있다. instance 변수가 실행 도중에 변경될 가능성이 없다면 위의 코드는 성능 관점에서는 상당히 비효율적이다. 다음과 같이 Sycnrhonized를 사용하지 않게끔 변경하는 것이 바람직하다.

private static instance = new Instance(...);

public static getInstance() { // <-- No Synchronized
return instance;
}

wait/nofiy 메소드를 이용한 Thread 동기화 방법에 대해서는 상당히 개발 경험이 많은 개발자들도 정확하게 이해하지 못하는 경우가 많은 것 같다. 특히 wait 메소드와 sleep 메소드를 정확하게 구분하지 못해 잘못된 코드를 작성하는 경우를 종종 보는데, 이 경우 동기화 문제에 의한 치명적인 성능 저하 현상을 유발할 수 있으므로 특히 주의해야 한다.
(이 블로그에서 API의 정확한 사용법을 일일시 설명하지는 않을 것이며 많은 좋은 자료들이 있으므로, 혹시라도 개념이 명확하지 않은 경우에는 반드시 사용법을 정확하게 이해했으면 한다)

Thread의 상태
Java 5의 Thread.getState() 메소드는 Thread의 현재 상태를 Thread.State Enum으로 리턴한다. Thread.State Enum의 정의는 아래와 같다.

BLOCKED
Thread state for a thread blocked waiting for a monitor lock.
NEW
Thread state for a thread which has not yet started.
RUNNABLE
Thread state for a runnable thread.
TERMINATED
Thread state for a terminated thread.
TIMED_WAITING
Thread state for a waiting thread with a specified waiting time.
WAITING
Thread state for a waiting thread.


BLOCKED 상태는 Sychronized에 의해 점유된 Monitor를 획득하지 못하고 기다리는 상태임을 의미한다. 반면 WAITING이나 TIMED_WAITING 상태는 wait 메소드를 이용해 대기하고 있음을 의미한다. RUNNABLE 상태는 현재 실제로 작업을 할 수 있는 상태이거나 작업을 수행 중인 상태를 의미한다.

이 상태들에 대한 정확한 이해가 Thread들간의 Lock 경합을 이해하는데 필수적이다. 만일 특정 Thread가 특정 Object의 Monitor를 장시간점유하고 있다면, 동일한 Monitor를 필요로 하는 다른 모든 Thread들은 BLOCKED 상태에서 대기하게 된다. 이 현상이 지나치게 되면 Thread 폭주가 발생하고 자칫 System 장애를 유발하게 된다. 이런 현상은 wait 메소드를 이용해 대기를 하는 경우도 마찬가지이다. 특정 Thread가 장시간 notify를 통해 Wait 상태의 Thread들을 깨워주지 않으면 수많은 Thread 들이 WAITING이나 TIMED_WAITING 상태에서 대기하는 현상이 발생하게 된다.

---------------------------------------------------------------------------------------------------------------------------------------------
다음 글에 계속...


신고
Trackback 0 : Comment 0

Write a comment

티스토리 툴바