본문 바로가기
Language/Java

[Java] 동기화

by 기몬식 2024. 3. 13.

동기화

WAS 에서는 많은 사용자의 동시 요청을 처리하기 위해 수십 ~ 수백 개의 쓰레드를 사용합니다. 두 개 이상의 쓰레드가 같은 자원을 이용할 때는 필연적으로 쓰레드 간의 경합이 발생하고 경우에 따라서는 Dead Lock 이 발생할 수 있습니다.


따라서 여러 쓰레드가 공유 자원을 사용할 때 정합성을 보장하려면 동기화 장치로 한 번에 하나의 쓰레드만 공유 자원에 접근할 수 있어야합니다. Java 에서는 Monitor 를 이용해 쓰레드를 동기화합니다.


모든 객체는 하나의 Monitor 를 소유하고 있으며 하나의 쓰레드만이 이를 획득할 수 있습니다. 특정 쓰레드가 소유한 Monitor 를 획득하기 위해서는 Monitor 소유권을 해제할 때까지 Wait Queue 에서 대기해야 합니다.


Mutal Exclusion & Critical Section

Heap 에는 객체의 맴버 변수가 있는데 JVM 은 해당 오브젝트와 클래스를 Object Lock(광의의 개념)을 사용해 보호합니다. 하나의 쓰레드만이 객체를 사용하게끔 내부적으로 Mutex 같은 것을 활용합니다. JVM 이 클래스 파일을 로드할 때 Heap 에는 Java.lang.class 의 instance 가 하나 생성되며 Object Lock 은 java.lang.class 객체의 instance 에 동기화 작업하는 것입니다.


자바의 동기화는 DMBS 의 Lock 과 좀 다른 것이 자바는 쓰레드가 무슨 작업을 하던 말던 동기화가 필요한 지역에 들어가면 무조건 동기화 작업을 수행합니다. 이런 동기화 작업을 수행하는 지역을 Critical Section 이라 하며 이 때 Lock 을 요청하는 방식입니다. Lock 을 획득하면 Critical Section 작업이 가능하며 획득에 실패하면 Lock 을 소유한 다른 쓰레드가 Lock 을 놓을 때까지 대기합니다.


그런데 자바는 객체에 대해 Lock 을 중복해서 획득하는 것이 가능합니다. 즉 쓰레드가 특정 객체의 Critical Section 에 진입할 때마다 Lock 을 획득하는 작업을 다시 수행합니다.



객체의 헤더에는 Lock Counter 를 가지고 있는데 Counter 가 0 일 때 Lock 을 획득할 수 있고 다른 쓰레드가 Lock 을 획득하면 1 증가, 놓으면 1 감소합니다.



이 중 Lock 정보를 담고 있는 헤더 부분을 Mark word 라고 부르며 벤더사별로 상이하나 일반적으로 32 bit 또는 64 bit 로 구성되어 있습니다. 객체가 아직 Unlock 상태라면 객체의 고유 정보를 담고 있습니다.


자바는 기본적으로 멀티 쓰레드 환경을 전제로 설계되었고 동기화 문제를 해결하기 위한 기본적인 메커니즘을 제공합니다. 자바에서 Object Lock 을 나타내는 Monitor 를 점유하는 유일한 방법은 synchronized 키워드를 사용하는 것인데 synchronized Statement 와 synchronized Method 두 가지 방법이 있습니다.

동기화 방법

자바에서 Monitor 는 특정 객체나 특정 코드 블록에 걸리는 일종의 Lock 이라고 생각해도 무방합니다. 자바는 Monitor 를 배타적 목적 외 공동작업을 위해서 사용합니다.

Synchronized Statement


    public void statement() {  
        System.out.println("enter");  
        synchronized (this) {  
            System.out.println("critical section");  
        }  
        System.out.println("exit");  
    }  

해당 객체에 대해 Monitor 를 점유하려는 모든 쓰레드는 같은 synchronized 구문이 실행되는 동안 대기 상태(BLOCKED)에 빠지게 됩니다. 또 위의 코드를 컴파일하여 Byte Code를 살펴보면 MONITORENTER , MONITOREXIT 라는 코드를 볼 수 있습니다.



마치 try ~ catch 문 처럼 임계영역 진입을 명시적으로 나타냅니다. MONITORENTER 가 실행되면 Stack 의 Object Reference 를 이용해 참조된(this) 객체에 대한 Lock 을 획득하는 작업을 수행합니다. Lock 을 이미 획득했다면 Lock Count 를 하나 증가시키며 Lock 을 획득하지 못하면 BLOCKED 상태로 대기하게 됩니다. MONITOREXIT 가 실행되면 Lock Count 를 감소 시키고 Lock 을 해제합니다.

Synchronized Method


public synchronized void reference() {  
    System.out.println("critical section");  
}

Synchronized Method 는 Method 를 선언할 때 synchronized 접근지정자를 사용하는 방식입니다. Synchronized Statement 방식과 달리 Byte Code 에 Monitor Lock 관련 내용이 없습니다. 왜냐하면 Synchronized Method 에 대해 Monitor Lock 의 사용여부는 메소드의 symbolic reference 를 resolution 하는 과정에서 결정되기 때문입니다. 즉 메소드 호출 자체가 임계 영역이란 것을 의미합니다. Synchronized Method 가 정상 실행 여부 상관없이 종료되기만 하면 JVM 은 Lock 을 자동으로 방출합니다.

Wait & Notify

한 쓰레드는 특정 데이터를 필요로 하고 다른 쓰레드는 특정 데이터를 제공하는 경우 Monitor Lock 을 사용해 쓰레드간 Cooperation 작업을 수행할 수 있습니다. 메신저 프로그램의 경우를 예로 들어 메시지를 받는 Listener 쓰레드 와 메시지를 보여주는 Reader 쓰레드가 있다고 가정하면 Listener 쓰레드는 Buffer 에 메시지를 기록하고 어느 정도 기록이 끝나면 Reader 쓰레드에게 메시지가 들어온 사실을 알려줘서 작업을 수행할 수 있도록 해줘야합니다. 이 때 쓰레드 간에는 Wait and Notify 형태의 Monitor Lock 을 사용하는 synchronized 의 응용 방식을 사용합니다.



위 그림은 cooperation 을 위한 Monitor Lock 을 표현한 것입니다. 자바에서 synchronized 키워드가 사용된 객체는 공유 객체가 됩니다. 공유 자원은 동기화된 메소드가 접근하는 공유 객체의 필드를 의미하며 동기화된 메소드들은 프로시저가 됩니다. 따라서 스레드가 모니터에 접근하기 위해서는 프로시저를 통해 접근해야합니다.


쓰레드가 Entry Set 으로 진입하면서 바로 Monitor Lock 획득을 시도합니다. 만약 다른 쓰레드가 이미 획득했다면 후발 쓰레드는 다시 Entry Set 에서 대기해야합니다. 만약 성공적으로 Monitor Lock 을 획득한다면 임계 영역에서 동기화 작업 후 Lock 을 Release 하게 됩니다.

정리

Monitor Lock 은 JVM 을 구현한 벤더마다 다른데 최근에는 성능상의 이유로 자주 사용하지 않는 추세입니다. 특히나 Monitor Lock 은 Heavy Weight Lock 상태로 OS 의 Mutex 와 조건 변수등으로 무겁게 구현하는 방식입니다.


반면에 Light Weight Lock 은 OS 자원을 이용하지 않는 Atomic Operation 을 이용하여 동기화를 처리합니다. Light Weight Lock 은 공유 객체의 Lock 정보가 담겨져 있는 오브젝트 헤더의 Mark Word 부분을 복사하는 과정에서 Lock 을 획득하게 됩니다. 이때 대기하던 쓰레드가 unlock 조건 확인 후 Lock 을 확보하는 과정은 간단한 코드 한 줄이라 할지라도 로우 레벨의 어셈블리어 명령어는 여러 개로 구성되어 있습니다. 때문에 이 과정에서 다른 쓰레드에게 Lock 을 찬탈 당할 수 있기 때문에 CAS(Compare and Swap) Operation 으로 하드웨어적 상호배제를 보장하게 됩니다.

멀티 스레드 환경에서 Lock 을 획득하는 과정의 원자성을 보장해주기 위해 자바에서는 다음과 같은 API 를 제공합니다.


출처


단 쓰레드에 간 경합이 심해져 대기하는 쓰레드에서 Spin Lock 이 발생하는 사태를 막고자 두 개 이상의 쓰레드가 공유 자원을 놓고 경쟁하게 되면 이전의 Heavy Weight Lock 으로 회귀하는 구조입니다.


오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

'Language > Java' 카테고리의 다른 글

[Java] Native Method  (0) 2024.06.15
[Java] 데이터 병렬 처리(Java 8)  (0) 2024.01.08
[Java] 데이터 병렬 처리(Java 5, 7)  (1) 2023.12.31
[Library] Assertions  (1) 2023.11.27
[Java] JPMS(Java 9 Platform Module System)  (1) 2023.10.23