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 가획득하려면해 Part 2 APM 361
당 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 되며명시적인작업은불 362 2013 기술백서 White Paper
필요하다. 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; Part 2 APM 363
생략 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; 생략 364 2013 기술백서 White Paper
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 Part 2 APM 365
메신저프로그램의경우클라이언트에서네트워크통해상대방의메시지를받는 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 획득을시도한 366 2013 기술백서 White Paper
다. 다른 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 와조건변수등으로무겁게구현을하는방식을말하고 lightweight Lock 은 Atomic operation 을이용한가벼운 Lock 으로서 Mutex 와같은 OS 의자원을사용하지않고내부의 Operation 만으로동기화를처리해 Monitor Lock 에비해가볍다는장점이있다. [ 그림 3] Heavy-weight Lock 과 Light-weight Lock Part 2 APM 367
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. Synchronized 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( 열거형 ) 으로선언되어있다. 368 2013 기술백서 White Paper
[ 그림 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 상태에서대기를하게된다. Part 2 APM 369
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 도같이동작을중지하게되는것이다. 좀더자세히분류하면아래와같다. VM 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 Thread 라는이름이부여된다. 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 를통해얻을수있던정보 쉽게얻을수있지만아직부족한면이많다 370 2013 기술백서 White Paper
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 의종류에따라더욱풍부한정보를같이제공한다. Part 2 APM 371
7.1 Thread Dump 생성방법 Unix 계열 : kill -3 [PID] Windows 계열 : 현재콘솔에서 Ctrl+Break. 공통 : jstack [PID] 7.2 Thread 덤프의정보 획득한 Thread 덤프에는다음과같은정보가들어있다. "pool-1-thread-13" prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000] java.lang.thread.state: RUNNABLE at java.net.socketinputstream.socketread0(native Method) at java.net.socketinputstream.read(socketinputstream.java:129) at sun.nio.cs.streamdecoder.readbytes(streamdecoder.java:264) at sun.nio.cs.streamdecoder.implread(streamdecoder.java:306) at sun.nio.cs.streamdecoder.read(streamdecoder.java:158) - Locked <0x0000000780b7e688> (a java.io.inputstreamreader) at java.io.inputstreamreader.read(inputstreamreader.java:167) at java.io.bufferedreader.fill(bufferedreader.java:136) at java.io.bufferedreader.readline(bufferedreader.java:299) - Locked <0x0000000780b7e688> (a java.io.inputstreamreader) at java.io.bufferedreader.readline(bufferedreader.java:362) Thread 이름 : Thread 의고유이름. java.lang.thread 클래스를이용해 Thread 를생성하면 Thread-(Number) 형식으로 Thread 이름이생성된다. java.util.concurrent.threadfactory 클래스를이용했으면 pool-(number)-thread-(number) 형식으로 Thread 이름이생성된다. 우선순위 : Thread 의우선순위 Thread ID: Thread 의 ID. 해당정보를이용해 Thread 의 CPU 사용, 메모리사용등유용한정보를얻을수있다. Thread 상태 : Thread 의상태. Thread 콜스택 : Thread 의콜스택 (Call Stack) 정보. 372 2013 기술백서 White Paper
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++)=""> Part 2 APM 373
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 를깨운다. 374 2013 기술백서 White Paper
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) Part 2 APM 375
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) 376 2013 기술백서 White Paper
"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 상태로분류된다 Part 2 APM 377
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 ------------ 378 2013 기술백서 White Paper
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 을점유하고있지않기때문에 ) 따라서정확한블로킹관계를해석하려면좀더면밀한분석이필요하다. Part 2 APM 379
아래내용은 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) 380 2013 기술백서 White Paper
"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 을잘못다루는문제는거의없어지고있다. Part 2 APM 381
참조문헌 김한도. Java Performance Fundamental. 서울 : 엑셈, 2009 http://ukja.tistory.com/ http://helloworld.naver.com/helloworld 382 2013 기술백서 White Paper