대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 JVM Synchronization
등록일 조회수 5215
첨부파일  

JVM Synchronization

㈜엑셈 컨설팅본부 /APM팀 김 정태



1. 개요

본 문서는 JAVA의 특징 중 하나인 Multi Thread 환경에서 공유 Resource에 대한 Thread 경합과 Synchronization 에 대한 내용들이 기술되어 있다. 본문 내용을 통해 Java의 동기화 장치인 Monitor에 대해 이해하고 나아가 Java의 Synchronization을 적절하게 활용할 수 있는 지식을 제공할 목적으로 작성되었다.


1.1 Java 그리고 Thread

WAS(Web Application Server)에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백 개의 Thread를 사용한다. 두 개 이상의 Thread가 같은 자원을 이용할 때는 필연적으로 Thread 간에 경합(Contention)이 발생하고 경우에 따라서는 Dead Lock이 발생할 수도 있다. 웹 애플리케이션에서 여러 Thread가 공유 자원에 접근하는 일은 매우 빈번하다. 대표적으로 로그를 기록하는 것도 로그를 기록하려는 Thread가 Lock을 획득하고 공유 자원에 접근한다. Dead Lock은 Thread 경합의 특별한 경우인데, 두 개 이상의 Thread에서 작업을 완료하기 위해서 상대의 작업이 끝나야 하는 상황을 말한다. Thread 경합 때문에 다양한 문제가 발생할 수 있으며, 이런 문제를 분석하기 위해서는 Thread Dump를 이용하기도 한다. 각 Thread의 상태를 정확히 알 수 있기 때문이다.


1.2 Thread 동기화

여러 Thread가 공유 자원을 사용할 때 정합성을 보장하려면 동기화 장치로 한 번에 하나의 Thread만 공유 자원에 접근할 수 있게 해야 한다. Java에서는 Monitor를 이용해 Thread를 동기화한다. 모든 Java 객체는 하나의 Monitor를 가지고 있다. 그리고 Monitor는 하나의 Thread만 소유할 수 있다. 특정 Thread가 소유한 Monitor를 다른 Thread가 획득하려면 해당 Monitor를 소유하고 있는 Thread가 Monitor를 해제할 때까지 Wait Queue에서 대기해야 한다.


1.3 Mutual Exclusion 과 Critical Section

공유 데이터에 다수의 Thread가 동시에 접근해 작업하면 메모리 Corruption 발생할 수 있다. 공유 데이터의 접근은 한번에 한 Thread씩 순차적으로 이루어 져야 한다. 누군가 공유 데이터를 사용할 때 다른 Thread들은 사용하지 못하도록 해야 하는데 예를 들면 쓰기 가능한 변수가 있다. Heap에는 Object의 멤버변수(Member Variable)가 있는데 JVM은 해당 Object와 Class를 Object Lock(광의의 개념)을 사용해 보호한다. Object Lock은 한번에 한 Thread만 Object를 사용하게끔 내부적으로 Mutex 같은 걸 활용한다. JVM이 Class File 을 Load할때 Heap에는 java.lang.class의 instance가 하나 생성되며 Object Lock은 java.lang.class Object의 instance에 동기화 작업하는 것이다. 이 Synchronization은 DBMS의 Lock과 좀 다르다. Oracle DBMS의 경우 Select는 Exclusive Lock을 안 걸지만(For update문 제외) DML일 경우에는 Exclusive Lock을 건다. 그러나 JAVA는 Thread가 무슨 작업하던 말던 Synchronization이 필요한 지역에 들어가면 무조건 Synchronization을 수행한다. 이 지역을 Critical Section이라고 하는데 Thread가 이 지역에 들어가면 반드시 동기화 작업을 수행한다. Thread가 Object의 Critical Section에 진입할 때 동기화를 수행해 Lock을 요청하는 방식이다. Lock을 획득하면 Critical Section에서 작업이 가능하며 Lock 획득에 실패하면 Lock을 소유한 다른 Thread가 Lock을 놓을 때까지 대기한다. 그런데 JAVA는 Object에 대해 Lock을 중복해서 획득하는 것이 가능하다. 즉 Thread가 특정 Object의 Critical Section에 진입할 때마다 Lock을 획득하는 작업을 다시 수행한다는 것이다.

Object의 Header에는 Lock Counter를 가지고 있는데 Lock을 획득하면 1증가, 놓으면 1감소한다. Lock을 소유한 Thread만 가능하다. Thread는 한번 수행에 한번의 Lock만을 획득하거나 놓을 수 있다. Count가 0일때 다른 Thread가 Lock을 획득할 수 있고 Thread가 반복해서 Lock을 획득하면 Count가 증가한다. Critical Section은 Object Reference와 연계해 동기화를 수행한다. Thread는 Critical Section의 첫 Instruction을 수행할 때 참조하는 Object에 대해 Lock을 획득해야 한다. Critical Section을 떠날 때 Lock은 자동 Release되며 명시적인 작업은 불필요하다. JVM을 이용하는 사람들은 단지 Critical Section을 지정해주기만 하면 동기화는 자동으로 된다는 것이다.


1.4 Monitor

Java는 기본적으로 Multi Thread 환경을 전제로 설계되었고 동기화 문제를 해결하기 위한 기본적인 메커니즘 제공한다. Java에서의 모든 Object는 반드시 하나의 Monitor를 가진다. 위에서 설명한 Object Lock이 Monitor에 해당한다. 특정 Object의 Monitor에는 동시에 하나의 Thread만이 들어갈 수(Enter) 있다. 다른 Thread에 의해 이미 점유된 Monitor에 들어가고자 하는 Thread는 Monitor의 Wait Set에서 대기한다. Java에서 Monitor를 점유하는 유일한 방법은 Synchronized 키워드를 사용하는 것인데 Synchronized Statement와 Synchronized Method 두 가지 방법이 있다. Synchronized Statement는 Method내 특정 Code Block에 Synchronized 키워드 사용한 것인데 Synchronized Statement를 사용하면 Critical Section에 들어가고 나올 때 Monitor Lock을 수행하는 작업이 Byte Code상에 명시적으로 나타나는 특징이 있다.


2. Java 의 동기화 (Synchronization) 방법

JAVA는 Monitor라는 Synchronization 메커니즘을 사용하는데 Monitor는 특정 Object나 특정 Code Block에 걸리는 일종의 Lock이라고 생각해도 무방하다. JAVA는 Monitor를 배타적 목적(Mutual Exclusion)외 공동작업(Cooperation)을 위해서 사용하기도 한다.


2.1 Synchronized Statement

... 생략 ... private int[] intArr = new int[10]; void synchBlock() { synchronized (this) { for (int i =0 ; i< intArr.length ; ++i ) { intArr(i) = i; } } } ... 생략 ...

Thread는 for 구문이 실행되는 동안 Object의 Monitor를 점유한다. 해당 Object에 대해 Monitor를 점유하려는 모든 Thread는 for구문이 실행되는 동안 대기 상태(BLOCKED)에 빠지게 된다. 앞서 설명했듯이 Byte Code를 보면 MONITORENTER, MONITOREXIT라는 Code를 볼 수 있다(생략). Synchronized Statement 에서는 이를 수행하는 Current Object를 대상으로 Monitor Lock을 수행한다. Byte Code에서 MONITORENTER가 실행되면 Stack의 Object Reference를 이용해 참조된(this) Object에 대한 Lock을 획득하는 작업을 수행한다. Lock을 이미 획득했다면 Lock Count를 하나 증가시키고 만약 처음 Lock을 획득하는 거라면 Lock Count를 1로 하고 Lock을 소유하게 된다. Lock을 획득할 상황이 아니면 Lock을 획득할 때까지 BLOCKED 상태로 대기하게 된다.

MONITOREXIT가 실행되면 Lock Count를 하나 감소시키고 만약 값이 0에 도달하면 Lock을 해제한다. MONITOREXIT은 Exception을 던지기 직전 Critical Section을 빠져 나오기 위해 사용되는데 Synchronized Statement의 사용은 내부적으로 try ~ catch절을 사용하는 효과가 있다고 한다. Monitor에 들어간 후 원하는 코드를 실행하고 다시 Monitor를 빠져 나오는 것이 Java가 동기화를 수행하는 방법이라고 할 수 있다.


2.2 Synchronized Method

... 생략 ... class SyncMtd { private int[] intArr = new int[10]; synchronized void syncMethod() { for (int i = 0 ; i < intArr.length ; ++i) { intArr[i] = i; } } } ... 생략 ...

http://blogfiles.naver.net/20130624_127/03jtk_1372001067447IqDlS_PNG/%C1%A6%B8%F1_%BE%F8%C0%BD.png Synchronized Method는 Method를 선언할 때 Synchronized 접근지정자(Qualifier)를 사용하는 방식이다. Synchronized Statement방식과 달리 Byte Code에 Monitor Lock관련 내용이 없다(MONITORENTER, MONITOREXIT). 왜냐하면 Synchronized Method에 대해 Monitor Lock의 사용여부는 Method의 symbolic reference를 resolution하는 과정에서 결정되기 때문이다.

이는 Method의 내용이 Critical Section이 아니고 Method의 호출 자체가 Critical Section 이란 것을 의미한다. Synchronized Statement는 런타임시점에 Monitor Lock을 획득하는 반면 Synchronized Method는 이 Method를 호출하기 위해 Lock을 획득해야 한다. Synchronized Method가 Instance Method라면 Method를 호출하는 this Object에 대해 Lock을 획득해야 한다. Class Method(Static Method)라면 이 Method가 속한 클래스, 즉 해당 Class의 Class Instance(Object)에 대해 Lock을 획득해야 한다. Synchronized Method가 정상 실행 여부 상관없이 종료되기만 하면 JVM은 Lock을 자동으로 Release 한다.


2.3 Wait And Notify

한 Thread는 특정 데이터를 필요로 하고 다른 Thread는 특정 데이터를 제공하는 경우 Monitor Lock을 사용해 Thread간 Cooperation작업을 수행할 수 있다.


[ 그림 1 ] Buffer Field


메신저 프로그램의 경우 클라이언트에서 네트워크 통해 상대방의 메시지를 받는 Listener Thread와 받아온 메시지를 보여주는 Reader thread가 있다고 가정해보자. Reader Thread는 Buffer의 메시지를 유저에게 보여주고 Buffer를 다시 비우고 버퍼에 메시지가 들어올 때까지 대기를 하게 된다. Listener Thread는 Buffer에 메시지를 기록하고 어느 정도 기록 끝나면 Reader Thread에게 메시지가 들어온 사실을 알려줘서 Reader Thread 가 메시지를 읽는 작업을 수행할 수 있도록 한다. 이때 Thread간에는 Wait and Notify 형태의 Monitor Lock을 사용한다. Wait와 Notify Method를 이용해서 동기화를 수행하는 방식은 Synchronized 방식의 "응용"이라고 할 수 있다.


[ 그림 2 ] Wait and Notify 방식


위 그림은 Cooperation을 위한 Monitor Lock을 표현한 것이다. Reader Thread는 Monitor Lock을 소유하고 있고 버퍼를 비운다음 wait()을 수행한다. 자신이 소유한 Monitor를 잠시 놓고 이 Monitor를 대기하는 Wait Set으로 들어간다. Listener Thread는 메시지를 받고 이를 유저가 읽어야 할 때쯤 notify() 수행하여 Wait Set에서 나와도 된다는 신호를 알리면(Lock release 는 아니다) Reader Thread가 Monitor Lock 바로 획득하지 못할 수도 있다. Listener Thread가 자발적으로 Monitor Lock을 놓지 않으면 누구도 Lock을 획득하지 못한다. Listener Thread가 notify() 이후 Lock을 놓으면 Reader Thread는 다시 Monitor Lock을 잡으려 한다. Thread간 Cooperation도 Mutual exclusion 처럼 Object Lock를 사용한다. 즉 Thread들은 특정 Object Class의 wait(), notify()등의 Method를 통해 Monitor Lock을 사용하는 것이다. Thread가 Entry set으로 진입하면 바로 Monitor Lock 획득을 시도한다. 다른 Thread가 Monitor Lock을 획득했다면 후발 Thread는 다시 Entry set에서 대기해야 한다. Monitor Lock을 획득해 Critical Section 코드를 수행하는 Thread는 Lock을 놓고 나가는 길 혹은 Wait set으로 들어가는 길이 있다. Monitor Lock을 소유한 Thread가 작업수행 중 wait()을 수행하면 획득했던 Monitor Lock 놓고 Wait set으로 들어간다.

그런데 이 Thread가 wait()만 수행하고 Wait set으로 들어가면 이 Monitor Lock을 획득할 수 있는 권한은 Entry set의 Thread들에게만 주어진다. 그러면 Entry set의 Thread들은 서로 경쟁해 Monitor Lock의 소유자가 된다. 따라서 notify(), notifyAll()을 수행해야 Entry set과 Wait set에 있는 Thread들이 경쟁하는 셈이다. notify()는 Wait set에 있는 Thread중 임의의 한 Thread만을 Monitor Lock 경합에 참여시키는 것이고 notifyAll()은 Wait set에 있는 모든 thread들을 경쟁에 참여시키는 것이다. Wait set에 들어온 Thread가 Critical Section을 벗어나는 방법은 Monitor를 다시 획득해 Lock을 놓고 나가는 방법 이외에는 없다. 모니터Lock은 JVM을 구현한 벤더마다 다른데 Java 동기화의 기본인 Monitor Lock은 성능상의 이유로 자주 사용하지 않는 것이 추세이다.

현재 우리가 사용하는 대부분의 JVM은 앞서 말한 Monitor Lock을 Heavy-weight Lock, Light-weight Lock으로 나누는 데 heavy-weight Lock은 Monitor Lock과 동일한 개념으로 각 Object에 대해 OS의 Mutex와 조건변수 등으로 무겁게 구현을 하는 방식을 말하고 light- weight Lock은 Atomic operation을 이용한 가벼운 Lock으로서 Mutex와 같은 OS의 자원을 사용하지 않고 내부의 Operation만으로 동기화를 처리해 Monitor Lock에 비해 가볍다는 장점이 있다.



[ 그림 3 ] Heavy - weight Lock 과 L ight - weight Lock


light-weight Lock은 대부분의 Object의 경우 Thread간의 경합아 발생하지 않는다는 점에 착안하여 만약 Thread간 경합 없이 자신이 Lock을 소유한 채 다시 Object의 Critical Section에 진입하면 light-weight Lock을 사용하여 Monitor enter, Monitor exit를 수행한다. 단 Thread간 경합이 발생하면 이전의 heavy-weight Lock으로 회귀하는 구조이다. light-weight Lock도 벤더마다 조금씩 다르게 구현되어 있다.


3. Sy n chronized Statement 와 Synchronized Method 사용

여러 Thread가 동시에 Access할 수 있는 객체는 무조건 Synchronized Statement/Method로 보호해야 하는가? 항상 그렇지는 않다. Synchronized를 수행하는 코드와 그렇지 않은 코드의 성능 차이는 대단히 큰데 동기화를 위해 Monitor에 액세스하는 작업에는 오버헤드가 따른다. 반드시 필요한 경우에만 사용해야 한다. 아래 예제를 살펴보자.


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

Singleton 방식을 구현하기 위해 getInstance Method를 Synchronized로 잘 보호했지만 불 필요한 성능감소가 있다. Instance변수가 실행 도중에 변경될 가능성이 없다면 위의 코드는 비효율적이다.


4. Thread 상태

Thread 덤프를 분석하려면 Thread의 상태를 알아야 한다. Thread의 상태는 java.lang.Thread 클래스 내부에 State라는 이름을 가진 Enumerated Types(열거형)으로 선언되어 있다.


[ 그림 4 ] Thread Status Diagram


. NEW: Thread 가 생성되었지만 아직 실행되지 않은 상태
. RUNNABLE: 현재 CPU 를 점유하고 작업을 수행 중인 상태 . 운영체제의 자원 분배로 인해 WAITING 상태가 될 수도 있다 .
. BLOCKED: Monitor 를 획득하기 위해 다른 Thread 가 Lock 을 해제하기를 기다리는 상태
. WAITING: wait () Method, join() Method, park() Method 등을 이용해 대기하고 있는 상태
. TIMED_WAITING: sleep() Method, wait() Method, join() Method, park() Method 등을 이용 해 대기하고 있는 상태 . WAITING 상태와의 차이점은 Method 의 인수로 최대 대기 시간을 명시할 수 있어 외부적인 변화뿐만 아니라 시간에 의해서도 WAITING 상태가 해제될 수 있다는 것이다 .

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


5. Thread 의 종류

Java Thread는 데몬 Thread(Daemon Thread)와 비 데몬 Thread(Non-daemon Thread)로 나눌 수 있다. 데몬 Thread는 다른 비 데몬 Thread가 없다면 동작을 중지한다. 사용자가 직접 Thread를 생성하지 않더라도 Java 애플리케이션이 기본적으로 여러 개의 Thread를 생성한다. 대부분이 데몬 Thread인데 Garbage Collection이나, JMX 등의 작업을 처리하기 위한 것이다. 'static void main(String[] args)' Method가 실행되는 Thread는 비 데몬 Thread로 생성되고, 이 Thread가 동작을 중지하면 다른 데몬 Thread도 같이 동작을 중지하게 되는 것이다. 좀 더 자세히 분류하면 아래와 같다.

. V M Background Thread: Compile, Optimization, Garbage Collection 등 JVM 내부의 일을 수행하는 Background Thread 들이다 .
. Main Thread: main(String[] args) Method 를 실행하는 Thread 로 사용자가 명시적으로 Thread 를 수행하지 않더라도 JVM 은 하나의 Main Thread 를 생성해서 Application 을 구 동한다 . Hot Spot JVM 에서는 VM Threa d 라는 이름이 부여된다 .
. User Thread: 사용자에 의해 명시적으로 생성된 Thread 들이다 . java.lang.Thread 를 상속 (extends) 받거나 , java.lang.Runnable 인터페이스를 구현 (implements) 함으로써 User Thread 를 생성할 수 있다 .


6. JVM 에서의 대기 현상 분석

Java/WAS 환경에서는 OWI와 같은 체계적인 방법론이 존재하지 않는다. Java에서는 다양한 방법을 제공하고 있다


6.1 Java 에서 제공하는 방법

. Thread Dump, GC Dump 와 같은 기본적인 툴
. BCI(Byte Code Instrumentation) + JVMPI/JVMTI ( C Interface )

Java 5에서 표준으로 채택된 JMX의 Platform MXBean, JVMpi/ti를 통해 얻을 수 있던 정보 쉽게 얻을 수 있지만 아직 부족한 면이 많다


6.2 WAS 에서 제공하는 방법

대부분의 WAS들이 사용자 Request를 효과적으로 처리하기 위해 Thread Pool, Connection Pool, EJB Pool/Cache와 같은 개념들을 구현했는데 이런 Pool/Cache들에서 대기 현상(Queuing)이 파악된다. 대부분의 WAS가 이런 류의 성능 정보(Pool/Cache 등의 사용량)를 JMX API를 통해 제공(Expose)하고 마음만 먹으면 자신만의 성능 Repository를 만들 수도 있다.


6.3 비효율 소스 튜닝에 따른 Side Effect

Application이 Loop를 돌면서 DML을 수행하는 구조에서 해당 작업을 여러 Thread가 동시에 수행한다고 할 때 Oracle 튜너가 이를 파악하고 모든 Application을 Batch Execution으로 변환하게끔(즉, PreparedStatement.addBatch, executeBatch를 사용하게끔) 유도를 하였다. DB와의 통신이 획기적으로 줄고 DB 작업 자체의 일 량도 줄어든다. 즉 Application 입장에서 보면 Wait Time(DB I/O Time)이 줄어들기 때문에 당연히 사용자의 Response Time은 감소해야 한다. 하지만 결과는 Application에서 극단적인 성능 저하가 발생하고 말았다. 그 이유는 두 가지가 있다. 첫째는 Batch Execution은 Application에서 더 많은 메모리를 요구한다. 이로 인해 Garbage Collection이 왕성하게 발생한다. 두 번째는 Batch Execution은 한번의 Operation에 Connection을 보유하는 시간이 좀 더 길다. 따라서 더 많은 Connection이 필요하고 그 만큼 Connection Pool이 금방 소진된다.

즉 Wait Time을 줄이려는 시도가 다른 Side Effect를 불러 오고 이로 인해 다른 종류의 Wait Time(GC Pause Time과 Connection Pool 대기 시간)이 증가한 경우이다. 동기화 메커니즘은 동시 세션/사용자/Thread를 지원하는 시스템에서는 공통적으로 사용된다. WAS Application에서는 수십 개 ~ 수백 개의 Thread가 동일한 자원을 획득하기 위해 경쟁을 하는데 이 과정에서 동기화 문제가 발생하고 대기 현상(Wait)도 발생할 수 있다.


7. Thread Dump

Java에서 Thread 동기화 문제를 분석하는 가장 기본적인 툴로서 현재 사용 중인 Thread의 상태와 Stack Trace를 출력하고 더불어 JVM의 종류에 따라 더욱 풍부한 정보를 같이 제공한다.


7.1 Thread Dump 생성 방법

. Unix 계열 : kill - 3 [PID]
. Windows 계열 : 현재 콘솔에 서 Ctrl+Break.
. 공통 : jstack [PID]


7.2 Thread 덤프의 정보

획득한 Thread 덤프에는 다음과 같은 정보가 들어 있다.


8. Case 별 Synchronized 에 대한 Thread Dump 분석

Case1: Synchronized에 의한 동기화

public class dump_test { static Object Lock = new Object(); public static void main(String[] args) { new Thread2().start(); try { Thread.sleep(10); } catch (Exception ex) { } new Thread1().start(); new Thread1().start(); new Thread1().start(); } } class Thread1 extends Thread { int idx = 1; public void run() { while (true) { Synchronized (dump_test.Lock) { // Thread1은 Synchronized 블록으로 인해 Thread2의 작업이 끝나기를 기다린다. System.out.println(idx++ + " loopn"); } } } } class Thread2 extends Thread { public void run() { while(true) { Synchronized(dump_test.Lock) { // Thread2는 Synchronized 블록을 이용해 긴(Long) 작업을 수행한다. for(int idx=0; idx<="" idx++)=""> } } } }



Case2: wait/notify에 의한 동기화

public class dump_test2 { static Object Lock = new Object(); public static void main(String[] args) { new Thread2().start(); try { Thread.sleep(10); } catch (Exception ex) {} new Thread1().start(); new Thread1().start(); new Thread1().start(); } } class Thread1 extends Thread { int idx = 1; public void run() { while (true) { Synchronized (dump_test2.Lock) { System.out.println(idx++ + " loopn"); try { dump_test2.Lock.wait(); } catch (Exception ex) {} // Wait Method를 이용해 notify가 이루어지기를 기다린다. } } } } class Thread2 extends Thread { public void run() { while (true) { for (int idx = 0; idx < 90000000; idx++) { } Synchronized (dump_test2.Lock) {dump_test2.Lock.notify(); // notify Method를 이용해 WAITING 상태의 Thread를 깨운다. } } } }



Case1(Synchronized)에서는 Thread1이 BLOCKED 상태에 있게 되며, Case2(Wait/Notify) 에서는 Thread1이 WATING 상태에 있게 된다. Java에서 명시적으로 Thread를 동기화시키는 방법은 이 두 개의 Case 뿐이다. Thread Pool 동기화에의 의한 Thread 대기, JDBC Connection Pool 동기화에 의한 Thread 대기, EJB Cache/Pool 동기화에 의한 Thread 대기 등 모든 Thread 대기가 이 두 개의 Case로 다 해석 가능하다. 위 두 가지 Case에 대해 각 벤더 별 Thread Dump에 어떻게 관찰되는지 확인해보자.



8.1 Hot Spot JVM
8.1.1 Case1: Synchronized에 의한 동기화

Full Thread dump Java HotSpot(TM) 64-Bit Server VM (1.5.0_04-b05 mixed mode): "DestroyJavaVM" prio=1 tid=0x0000000040115580 nid=0x1e18 waiting on condition [0x0000000000000000..0x0000007fbfffd380] "Thread-3" prio=1 tid=0x0000002afedbd330 nid=0x1e27 waiting for Monitor Entry [0x00000000410c9000..0x00000000410c9bb0] at Thread1.run(dump_test.java:22) - waiting to Lock < 0x0000002af44195c8> (a java.lang.Object) "Thread-2" prio=1 tid=0x0000002afeda6900 nid=0x1e26 waiting for Monitor Entry [0x0000000040fc8000..0x0000000040fc8c30] at Thread1.run(dump_test.java:22) - waiting to Lock < 0x0000002af44195c8> (a java.lang.Object) "Thread-1" prio=1 tid=0x0000002afeda5fe0 nid=0x1e25 waiting for Monitor Entry [0x0000000040ec7000..0x0000000040ec7cb0] at Thread1.run(dump_test.java:22) - waiting to Lock < 0x0000002af44195c8> (a java.lang.Object) "Thread-0" prio=1 tid=0x0000002afeda3520 nid=0x1e24 runnable [0x0000000040dc6000..0x0000000040dc6d30] at Thread2.run(dump_test.java:38) - waiting to Lock < 0x0000002af44195c8> (a java.lang.Object)



Synchronized에 의한 Thread 블로킹이 발생하는 도중의 Thread dump 결과이다. Thread-1, Thread-2, Thread-3이 "waiting for Monitor Entry" 상태이다. 즉 Synchronized 문에 의해 블로킹되어 Monitor에 들어가기 위해 기다리고 있는 상태다, 이 경우 Thread.getState() Method는 BLOCKED 값을 Return하는 반면 Thread-0는 현재 "runnable" 상태로 일을 하고 있는 중이다. 또한 Thread-0과 Thread1,2,3 이 동일한 0x0000002af44195c8에 대해 경합을 하고 있다.


8.1.2 Case2: Wait/Nofity 에 의한 동기화

Full Thread dump Java HotSpot(TM) 64-Bit Server VM (1.5.0_04-b05 mixed mode): "DestroyJavaVM" prio=1 tid=0x0000000040115580 nid=0x1c6c waiting on condition [0x0000000000000000..0x0000007fbfffd380] "Thread-3" prio=1 tid=0x0000002afedb7020 nid=0x1c7b in Object.wait() [0x00000000410c9000..0x00000000410c9db0] at java.lang.Object.wait(Native Method) - waiting on < 0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474) at Thread1.run(dump_test2.java:23) - Locked < 0x0000002af4442a98> (a java.lang.Object) "Thread-2" prio=1 tid=0x0000002afedb5830 nid=0x1c7a in Object.wait() [0x0000000040fc8000..0x0000000040fc8e30] at java.lang.Object.wait(Native Method) - waiting on < 0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474) at Thread1.run(dump_test2.java:23) - Locked < 0x0000002af4442a98> (a java.lang.Object) "Thread-1" prio=1 tid=0x0000002afeda6d10 nid=0x1c79 in Object.wait() [0x0000000040ec7000..0x0000000040ec7eb0] at java.lang.Object.wait(Native Method) - waiting on < 0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474) at Thread1.run(dump_test2.java:23) - Locked < 0x0000002af4442a98> (a java.lang.Object) "Thread-0" prio=1 tid=0x0000002afeda3550 nid=0x1c78 runnable [0x0000000040dc6000..0x0000000040dc6b30] at Thread2.run(dump_test2.java:36)

Hot Spot VM 에서 Wait/Notify 에 의한 Thread 블로킹이 발생하는 도중의 Thread dump 결과 이다 . Synchronized 에 의한 Thread 블로킹의 사례와 달리 BLOCKED 상태가 아닌 WAITING 상태에서 대기한다 . 여기서 특별히 주의해 야 할 것은 Thread1,2,3 을 실제로 블로킹하고 있는 Thread 가 정확하게 어떤 Thread 인지 직관적으로 알 수 없다는 것이다 . Thread1,2,3 은 비록 대기상태에 있지만 , 이는 블로킹에 의한 것이 아니라 단지 Notify 가 오기를 기다릴 (Wait) 뿐이 기 때문이다 . BLOCKED 상태와 WAITING 상태의 정확한 차이를 이해해야 한다 .

참고로 BLOCKED 상태와 WAITING 상태의 정확한 차이를 이해하려면 다음 코드가 의미하는 바를 이 해할 필요가 있다 .


Synchronized(LockObject) { LockObject.wait(); doSomething(); }

위의 코드가 의미하는 바는 다음과 같다. Lock Object의 Monitor에 우선 들어간다. Lock Object에 대한 점유권을 포기하고 Monitor의 Wait Set(대기 리스트)에서 대기한다. 다른 Thread가 Notify를 해주면 Wait Set에서 나와서 다시 Lock Object를 점유한다. 만일 다른 Thread가 이미 Lock Object를 점유했다면 다시 Wait Set에서 대기한다. Lock Object를 점유한 채 doSomething()을 수행하고, Lock Object의 Monitor에서 빠져 나온다. 즉, Lock Object.wait() Method 호출을 통해 대기하고 있는 상태에서는 이미 Lock Object에 대한 점유권을 포기한(Release) 상태이기 때문에 BLOCKED 상태가 아닌 WAITING 상태로 분류되는 반면 Synchronized 문장에 의해 Monitor에 아직 들어가지도 못한 상태에서는 BLOCKED 상태로 분류된다


8.2 IBM JVM

IBM JVM의 Thread Dump는 Hot Spot JVM에 비해서 매우 풍부한 정보를 제공하는데 단순히 Thread들의 현재 상태뿐 아니라, JVM의 상태에 대한 여러 가지 정보를 제공한다.


8.2.1 Case1: Synchronized 에 의한 동기화

// 모니터 정보 1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated Object-Monitors): ... 2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x00000000: 3LKMONObject java.lang.Object@30127640/30127648: Flat Locked by Thread ident 0x08, Entry Count 1 // < -- 오브젝트가 0x08 Thread에 의해 Locking 3LKNOTIFYQ Waiting to be notified: // < -- 세 개의 Thread가 대기 중 3LKWAITNOTIFY "Thread-1" (0x356716A0) 3LKWAITNOTIFY "Thread-2" (0x356F8020) 3LKWAITNOTIFY "Thread-3" (0x3577FA20) ... // Java Object Monitor 정보 1LKOBJMONDUMP Java Object Monitor Dump (flat & inflated Object-Monitors): ... 2LKFLATLockED java.lang.Object@30127640/30127648 3LKFLATDETAILS Locknflags 00080000 Flat Locked by Thread ident 0x08, Entry Count 1 ... // Thread 목록 1LKFLATMONDUMP Thread identifiers (as used in flat Monitors): 2LKFLATMON ident 0x02 "Thread-4" (0x3000D2A0) ee 0x3000D080 2LKFLATMON ident 0x0B "Thread-3" (0x3577FA20) ee 0x3577F800 2LKFLATMON ident 0x0A "Thread-2" (0x356F8020) ee 0x356F7E00 2LKFLATMON ident 0x09 "Thread-1" (0x356716A0) ee 0x35671480 2LKFLATMON ident 0x08 "Thread-0" (0x355E71A0) ee 0x355E6F80 < -- 30127640/30127648을 점유하고 있는 0x08 Thread의 이름이 Thread-0 ... // Threrad Stack Dump 2XMFULLTHDDUMP Full Thread dump Classic VM (J2RE 1.4.2 IBM AIX build ca142-20050929a (SR3), native Threads): 3XMThreadINFO "Thread-4" (TID:0x300CB530, sys_Thread_t:0x3000D2A0, state:CW, native ID:0x1) prio=5 // < -- Conditional Wait 상태 3XHNATIVESTACK Native Stack NULL ------------ 3XHSTACKLINE at 0xDB84E184 in xeRunJavaVarArgMethod ... 3XMThreadINFO "Thread-3" (TID:0x300CB588, sys_Thread_t:0x3577FA20, state:CW, native ID:0xA0B) prio=5 4XESTACKTRACE at Thread1.run(dump_test.java:21) ... 3XMThreadINFO "Thread-2" (TID:0x300CB5E8, sys_Thread_t:0x356F8020, state:CW, native ID:0x90A) prio=5 4XESTACKTRACE at Thread1.run(dump_test.java:21) ... 3XMThreadINFO "Thread-1" (TID:0x300CB648, sys_Thread_t:0x356716A0, state:CW, native ID:0x809) prio=5 4XESTACKTRACE at Thread1.run(dump_test.java:21) ... 3XMThreadINFO "Thread-0" (TID:0x300CB6A8, sys_Thread_t:0x355E71A0, state:R, native ID:0x708) prio=5 // < -- Lock 홀더 4XESTACKTRACE at Thread2.run(dump_test.java(Compiled Code)) 3XHNATIVESTACK Native Stack NULL ------------ 3XHSTACKLINE at 0x344DE720 in ...

"Thread-0(ident=0x08)" Thread가 java.lang.Object@30127640/30127648 오브젝트에 대해 Monitor Lock을 점유하고 실행(state:R) 중이며, 나머지 세 개의 Thread "Thread 1,2,3"은 동일 오브젝트에 대해 Lock을 획득하기 위해 대기(Conditional Waiting)상태이다.


8.2.2 Case2: Wait/Nofity 에 의한 동기화

Wait/Nofity에 의한 동기화에 의해 Thread 블로킹이 발생하는 경우에는 한가지 사실을 제외하고는 Case1과 완전히 동일하다. Wait/Notify에 의한 동기화의 경우 실제로 Lock을 점유하고 있는 Thread는 존재하지 않고, Nofity 해주기를 대기할 뿐이다. 따라서 Lock을 점유하고 있는 Thread가 어떤 Thread인지에 대한 정보가 Thread Dump에 나타나지 않는다.(실제로 Lock을 점유하고 있지 않기 때문에) 따라서 정확한 블로킹 관계를 해석하려면 좀더 면밀한 분석이 필요하다.

아래 내용은 Wait/Notify에 의한 Thread 동기화가 발생하는 상황의 Thread Dump의 일부이다. Wait/Notify에 의한 Thread 동기화의 경우에는 Lock을 점유하고 있는 Thread의 정체를 바로 알 수 없다. (블로킹이 아닌 단순 대기이기 때문에)


... 1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated Object-Monitors): ... 2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x3003BAC0: 3LKMONObject java.lang.Object@30138C58/30138C60: // < -- Object에 대해 Waiting Thread가 존재하지만 Locking 되어 있지는 않다!!! 3LKNOTIFYQ Waiting to be notified: 3LKWAITNOTIFY "Thread-3" (0x3577F5A0) 3LKWAITNOTIFY "Thread-1" (0x355E7C20) 3LKWAITNOTIFY "Thread-2" (0x356F7A20)

9. Thread Dump 를 통한 Thread 동기화 문제 해결의 실 사례

실제 운영 환경에서 성능 문제가 발생한 경우에 추출한 것으로 Thread Dump를 분석한 결과 많은 수의 Worker Thread들이 다음과 같이 블로킹되어 있었다.


"http8080-Processor2" daemon prio=5 tid=0x042977b0 nid=0x9a6c in Object.wait() [503f000..503fdb8] at java.lang.Object.wait(Native Method) - waiting on < 0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool) at java.lang.Object.wait(Object.java:429) at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(Unknown Source) - Locked < 0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool) at org.apache.commons.dbcp.PoolingDriver.connect(PoolingDriver.java:146) at java.sql.DriverManager.getConnection(DriverManager.java:512) - Locked < 0x507dbb58> (a java.lang.Class) at java.sql.DriverManager.getConnection(DriverManager.java:193) - Locked < 0x507dbb58> (a java.lang.Class) at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40) at org.apache.jsp.managerInfo_jsp._jspService(managerInfo_jsp.java:71) ... at org.apache.tomcat.util.Threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683) at java.lang.Thread.run(Thread.java:534) "http8080-Processor1" daemon prio=5 tid=0x043a4120 nid=0x76f8 waiting for Monitor Entry [4fff000..4fffdb8] at java.sql.DriverManager.getConnection(DriverManager.java:187) - waiting to Lock < 0x507dbb58> (a java.lang.Class) at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40) at org.apache.jsp.loginOK_jsp._jspService(loginOK_jsp.java:130) at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:137) at javax.servlet.http.HttpServlet.service(HttpServlet.java:853) at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:210) at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:295) at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:241) ... at org.apache.tomcat.util.Threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683) at java.lang.Thread.run(Thread.java:534) ...

위의 Thread Dump를 분석해보면 java.sql.DriverManager.getConnection() 내부에서 Connection을 얻는 과정에서 Synchronized에 의한 Thread 블로킹이 발생했다. org.apache.commons.pool.impl.GenericObjectPool.borrowObject() 내부에서 Connection을 얻는 과정에서 Wait/Notify에 의한 Thread 블로킹이 발생했다. 즉, Connection Pool에서 Connection을 얻는 과정에서 Thread 경합이 발생한 것으로 이는 현재 Connection Pool의 완전히 소진되었고 이로 인해 새로운 DB Request에 대해 새로운 Connection을 맺는 과정에서 성능 저하 현상이 생겼다는 것이다. 만일 Connection Pool의 최대 Connection 수가 낮게 설정되어 있다면 대기 현상은 더욱 심해질 것이다. 다른 Thread가 DB Request를 끝내고 Connection을 놓을 때까지 기다려야 하기 때문이다.

해결책은? Connection Pool의 초기 Connection 수와 최대 Connection수를 키운다. 만일 실제 발생하는 DB Request수는 작은데 Connection Pool이 금방 소진된다면 Connection을 닫지 않는 문제일 가능성이 크다. 이 경우에는 소스 검증이나 모니터링 툴을 통해 Connection을 열고 닫는 로직이 정상적으로 작동하는지 검증해야 한다. 참고로 다행히 iBatis나 Hibernate같은 프레임 워크들이 보편적으로 사용되면서 JDBC Connection을 잘못 다루는 문제는 거의 없어지고 있다.


참조문헌

김한도. Java Performance Fundamental. 서울: 엑셈, 2009
http://ukja.tistory.com/
http://helloworld.naver.com/helloworld



출처 : (주)엑셈

제공 : DB포탈사이트 DBguide.net