////
Search

3. JVM Sychronization 이란?

Created
2022/09/17 13:45
Tags
JVM

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]

6.2 Thread Dump 정보 의미