1. 개요
1.1 Java 그리고 Thread
•
WAS에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백개의 Thread를 사용한다.
•
두 개 이상의 Thread가 같은 자원을 이용할 때는 필연적으로 Thread간에 경합이 발생한다.
◦
경우에 따라 Dead Lock이 발생할 수도 있다.
◦
대표적으로 로그를 기록하려는 Thread가 Lock을 획득하고 공유 자원에 접근한다.
•
Thread 경합 때문에 다양한 문제가 발생할 수 있다.
•
이런 문제를 분석하기 위해서 Thead Dump를 이용하기도 한다.
1.2 Thread 동기화
•
여러 Thread가 공유 자원을 사용할 때 정합성을 보장하려면 동기화 장치로 한 번에 하나의 Thread만 공유 자원에 접근할 수 있게 해야 한다.
•
Java에서는 Monitor를 이용해 Thread를 동기화 한다.
•
모든 Java 객체는 하나의 Monitor을 가지고 있다.
◦
Monitor는 하나의 Thread만 소유할 수 있다.
•
특정 Thread가 소유한 Monitor를 다른 Trhead가 획득하려면 해당 Monitor를 소유하고 있는 스레드가 Monitor를 해재헤라 때까지 Wait Queue에서 대기해야 한다.
1.3 Mutual Exclusion과 Critical Section
•
공유 데이터에 다수의 스레드가 동시에 접근하여 작업하면 메모리 Corruption이 발생할 수 있다.
•
공유 데이터의 접근은 한번에 한 Thread씩 순차적으로 이루어져야 한다.
•
Heap에는 Object의 멤버 변수가 있는데 JVM은 해당 Object와 Class를 Object Lock(광의적 개념)을 사용해 보호한다.
•
Object Lock은 한번에 한 스레드만 Object를 사용하게끔 내부적으로 Mutex같은 걸 활용 한다.
•
JVM이 Class File을 Load할 때 Heap에는 Java.lang.class의 인스턴스가 하나 생성되며 Object Lock은 Java.lang.class Object의 인스턴스에 동기화 작업하는 것이다.
•
이 Synchronization은 DBMS의 Lock과는 좀 다르다.
◦
오라클의 경우 SELECT는 배타적 Lock을 안걸지만, DML일 경우 배타적 락을 건다.
◦
Java는 Thread가 무슨 작업을 하던 동기화가 필요한 지역에 들어가면 무조건 동기화를 수행한다.
▪
이 지역을 Critical Seciton이라 하는데 스레드가 이 지역에 들어가면 반드시 동기화가 필요하다.
◦
스레드가 Object의 임계 영역에 진입할 때 동기화를 수행해 Lock을 요청하는 방식
▪
Lock을 획득하면 임계 영역에서 작업이 가능하며
▪
Lock획득에 실해하면 Lock을 소유한 다른 스레드가 Lock을 놓을 때까지 대기한다.
◦
Java는 Object에 대해 Lock을 중복해서 획득하는게 가능
▪
스레드가 특정 Object의 임계영역에 진입할 때마다 Lock을 획득하는 작업을 다시 수행한다는 것이다.
▪
Object의 Header에는 Lock Counter를 가지고 있다.
•
Lock 획득시엔 +1 Lock 해제시엔 -1
•
Lock을 소유한 스레드만 가능하다.
•
Count가 0일때 다른 스레드가 Lock을 획득할 수 없고 스레드가 반복해 Lock을 획득하면 카운트가 증가한다.
•
Critical Section은 Object Reference와 연계해 동기화를 수행한다.
◦
스레드는 임계영역의 첫 Instruction을 수행할 때 참조하는 Object에 대해 Lock을 획득해야 한다.
◦
임계영역을 떠날 때 Lock은 자동 Release되며 명시적인 작업은 불필요
◦
임계영역을 지정해주면 동기화는 자동으로 이뤄짐
1.4 Monitor
•
Java는 기본적으로 멀티 스레드 환경을 전제로 설계되었고 동기화 문제를 해결하기 위한 기본적 매커니즘을 제공한다.
•
앞서 설명한 Object Lock이 Monitor에 해당한다.
•
특정 Object의 모니터에는 동시에 하나의 스레드만이 들어갈 수 있다.
•
다른 스레드에 의해 이미 점위된 모니터에 들어가고자 하는 스레드는 모니터의 Wait Set에서 대기한다.
•
모니터를 점유하는 유일한 방법은 synchronized 키워드를 사용하는 것
◦
synchronized는 Statement와 Method 두 가지 방법이 있다.
◦
Statement는 메소드 내 특정 코드에 synchronized 키워드를 사용한 것
▪
락을 수행하는 작업이 바이트코에 명시적으로 나타나는 특징이 있음
◦
Method는 메소드 전체에 synchronized키워드를 사용한 것
2. Java의 동기화 방법
2.1 Synchronized Statement
...
private int[] intArr = new int[10];
void syncBlock() {
synchronized (this) {
for (int i = 0; i < intArr .length; i++) {
intArr[i] = i;
}
}
}
...
Java
복사
•
스레드는 for 구문이 실행되는 동안 Object의 모니터를 점유한다.
•
해당 Object에 대해 모니터를 점유하려는 모든 스레드는 for구문이 실행되는 동안 대기 상태에 빠진다.
•
바이트코드에서 MONITORENTER가 실행되면 Lock Count를 1증가시킨다.
•
MONITOREXIT을 실행하게 되면 Lock Count를 하나 감소시킨다.
•
Synchronized Statement의 사용은 내부적으로 try ~ catch 절을 사용하는 효과가 있다.
◦
획득시에 획득을 못하면 대기상태로, 감소시에 카운트가 0이면 락이 해제되기 때문
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;
}
}
}
...
Java
복사
•
Synchronized Method는 Method를 선언할 때 Synchronized 접근지정자를 사용하는 방식이다.
•
이 방식은 바이트코드에 모니터 Lock관련 내용이 없다.
•
왜냐하면 Synchronized Method에 대해 모니터 락의 사용 여부는 메소드 symbolic reference를 resolution하는 과정에서 결정되기 때문이다.
•
메소드 내용이 임계영역이 아니고 메소드의 호출 자체가 임계영역이란 것을 의미한다.
•
이 방법은 메소드를 호출하기 위해 Lock을 획득해야 한다.
•
메소드 동기화가 Instance Method라면 Method를 호출하는 this Object에 대해 Lock을 획득해야 한다.
•
Synchronized Method가 정상 여부 상관없이 종료되기만 하면 JVM은 Lock을 자동으로 해제한다.
2.3 Wait And Notify
•
한 스레드는 특정 데이터를 필요로 하고 다른 스레드는 특정 데이터를 제공하는 경우 Monitor Lock을 사용해 스레드간 Cooperation 작업을 수행할 수 있다.
•
메신저 프로그램의 경우 클라이언트에서 네트워크를 통해 상대방의 메시지를 받는 Listener 스레드와 받아온 메시지를 보여주는 Reader 스레드가 있다고 가정해보자.
•
Reader 스레드는 버퍼의 메시지를 유저에게 보여주고 버퍼를 다시 비우고 버퍼에 메시지가 들어올 때까지 대기하게 된다.
•
Listener 스레드는 버퍼에 메시지를 기록하고 어느 정도 기록이 끝나면 Reader 스레드에게 메시지가 들어온 사실을 알려줘 Reader 스레드가 메시지를 읽는 작업을 수행할 수 있도록 한다.
•
이때 스레드간 Wait and Notify 형태의 모니터 락을 사용한다.
◦
Wait과 Notify Method를 이요해 동기화를 수행하는 방식은 Synchronized의 응용이라 할 수 있다.
•
Cooperation을 위한 모니터 락을 표현한 것이다.
•
Reader 스레드는 모니터락을 소유하고 있고 버퍼를 비운 다음 wait()을 수행한다.
◦
자신이 소유한 모니터를 잠시 놓고 이 모니터를 대기하는 Wait Set으로 들어간다.
•
Listener 스레드는 메시지를 받고 이를 유저가 읽어야 할 때쯤 notify()를 수행하여 Wait Set에서 나와도 된다는 신호를 알리면 Reader 스레드가 모니터락을 바로 획득하지 못할 수도 있다.
◦
Listener 스레드가 자발적으로 모니터 락을 놓지 않으면 누구도 모니터 락을 획득할 수 없다.
◦
스레드간 Cooperation도 Mutual exclusion처럼 Object Lock를 사용한다.
◦
즉 스레드들은 특정 Object Class의 wait(), notify()등의 Method를 통해 모니터 락을 사용하는 것
•
스레드가 Entry Set으로 진입하면 바로 모니터 락 획득을 시도한다.
◦
다른 스레드가 모디터 락을 획득했다면 후발 스레드는 엔트리 셋에서 대기해야 한다.
◦
모니터락을 소유한 스레드가 작업 수행 중 wait()을 수행하면 획득했던 모니터락을 놓고 Wait Set으로 들어간다.
•
wait()만 수행하고 Wait set으로 들어가면 이 모니터 락을 획득할 수 있는 권한은 Entry set의 스레드들에게만 주어진다.
•
notify()는 Wait set에 있는 스레드 중 임의의 한 스레드만을 모니터 락 경합에 참여시킨다.
•
notifyAll()은 모든 스레드들을 경쟁에 참여시킨다.
•
Wait set에 들어온 스레드가 임계영역을 벗어나는 방법은 모니터를 다시 획득해 락을 놓고 나가는 방법 이외에는 없다.
◦
JVM 벤더마다 다르지만 모니터 락은 성늘상의 이유로 자주 사용하지 않는 것이 추세이다.
•
앞서 말한 모니터락을 Heavy-weight Lock, Light-weight Lock 으로 나눈다.
◦
Heavy-weight Lock은 모니터 락과 동일한 개념
▪
뮤텍스 같은 OS 자원을 이용하지 않고 Operation 만으로 동기화 처리해 모니터 락에 비해 가벽다는 장점이 있다.
◦
Light-weight Lock은 Atomic operation을 이용한 가벼운 Lock으로 뮤텍스와 같은 OS의 자원을 사용하지 않고 내부의 Operation 만으로 동기화 처리해 모니터 락에비해 가볍다는 장점이 있다.
▪
Object의 경우 스레드간의 경합이 발생하지 않는다는 점에 착안하여 만약 스레드간 경합 없이 자신이 락을 소유한 채 다시 Object
2.4 Synchronized Statement와 synchronized Method 사용
•
여러스레드가 동시에 Access 할 수 있는 객체는 무조건 Synchronized Statement/Method로 보호해야 하는걸까?
◦
항상 그렇지는 않다.
◦
Synchronized를 수행하는 코드와 그렇지 않은 코드의 성능 차이는 대단히 큰데 동기화를 위해 모니터에 액세스하는 작업에는 오버헤드가 따른다.
private static Instance instance = null;
public static synchronized getInstace() {
if(instance == null)
instace = new Instance(...);
return instance;
}
Java
복사
•
Singleton 방식을 구현하기 위해 getInstance Method을 Synchronized로 잘 보호했지만 불필요한 성능감소가 있다.
•
Instance 변수가 실행 도중에 변경될 가능성이 없다면 위 코드는 비효율적이다.
3. Thread 상태
•
Thread dump를 분석하려면 스레드의 상태를 알아야 한다.
•
Thread의 상태는 Java.lang.Thread클래스 내부에 State이름을 가진 열거타입으로 선언되어 있다.
•
NEW
◦
Thread가 생성되었지만 아직 실행되지 않은 상태
•
RUNNABLE
◦
현재 CPU를 점유하고 작업을 수행중인 상태
◦
운영체제의 자원 분배로 인해 WAITING상태가 될 수 있다.
•
BLOCKED
◦
모니터를 획득하기 위해 다른 Thread가 Lock을 해제할 때까지 기다리는 상태
•
WAITING
◦
wait(), join(), park() 메소드 등등을 이용해 대기하는 상태
•
TIMED_WAITING
◦
sleep(), wait(), join(), park() 등등 의 메소드를 이용해 대기하는 상태
◦
WAITING과 다른점은 메소드 인수로 최대 대기 시간을 명시할 수 있다.
◦
외부적인 변화뿐 아니라 시간에 의해 WAITING상태가 될 수도 있다.
4. Thread의 종류
•
자바의 Thread는 데몬 Thread와 비데몬 Thread로 나뉜다.
•
데몬 스레드는 다른 비데몬 스레드가 없다면 동작을 중지한다.
•
사용자가 직접 스레드를 생서하지 않더라도 자바는 기본적으로 여러 스레드를 생성한다.
•
대부분이 데몬 스레드로 GC나 JMX 등 작업을 처리하기 위한 것이다.
•
VM Background Thread
◦
Compile, Optimization, Garbage Collection 등 JVM내부의 일을 수행하는 백그라운드 스레드
•
Main Thread
◦
main 메소드를 실행하는 Thread
◦
사용자가 명시적으로 스레드를 수행하지 않더라도 JVM이 Main Thread를 생성
◦
Hotspot JVM에서는 VM Thread라는 이름이 부여
•
User Thread
◦
사용자에 의해 명시적으로 생성된 Thread 들이다.
◦
Java.lang.Thread 를 상속받거나, Java.lang.Runnable 인터페이스를 구현함으로써
스레드를 생성할 수 있다.
5. JVM에서의 대기 현상 분석
5.1 Java에서 제공하는 방법
•
Thread Dump, GC Dump와 같은 기본적인 툴
•
BCI (Bytecode Instrumentation) + JVMP/TI (C Interface)
•
Java 5 에서 표준으로 채택된 JMX의 Platform MXBean, JVMPI/TI를 통해 얻을수 있던 정보를 쉽게 획득 가능하지만 아직 부족한 면이 많다.
•
대부분의 WAS는 Thread Pool, Connection Pool, EJB Pool/Cache와 같은 개념들을 구현했는데 이런 Pool/Cache들에서 대기 현상이 파악된다.
•
대부분의 WAS가 이런 성능정보를 JMX API를 통해 제공하고 맘만 먹으면 자신의 성능 Repository를 만들 수도 있다.
5.2 비효율 소스 튜닝에 따른 Side Effect
•
Loop을 돌면서 DML을 수행하는 구조에서 해당 작업을 여러 Thread가 동시에 수행한다고 할 때 오라클 튜너가 이를 파악하고 모든 App을 Batch Execution으로 변환하게 끔 유도를 하였다.
•
DB와 통신이 획기적으로 줄고 DB 작업 자체의 일량도 줄어들었기 때문에 선능이 증가해야하지만 APP에서 극단적인 성능저하가 발생하게 된다.
•
이유로는 두 가지가 있다.
◦
Batch Execution은 APP에서 더 많은 메모리를 요구한다.
▪
GC가 더 왕성하게 발생한다.
◦
Batch Execution은 한번의 Operation에 Connection을 보유하는 시간이 좀 더 길다.
▪
더 많은 Connection이 필요하기 때문에 Connection Pool이 금방 소진된다.
6. Thread Dump
6.1 Thread Dump 생성 방법
•
Linux/Unix 계열 = kill -3 [PID]
•
Windows 계열 = 콘솔에서 Ctrl + Break
•
공통 = jstack [PID]