개발/Java

자바에서 모니터가 동작하는 원리 (with synchronized)

Debin 2023. 12. 28.
반응형

모니터란

모니터란 뮤텍스나 세마포어보다 더 고수준의 동기화 기법이다.

전공 수업에서는 세마포어를 편리하게 사용하기 위해 인터페이스를 제공한 것이 모니터라고 배웠었다.

부족하지만 수업을 듣고 정리해 놓은 글이 있다.

 

https://devdebin.tistory.com/16#%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4-1

 

임계 구역과 임계 구역 해결 방법

2021. 10. 2. 00:51 2022. 6. 22. 15:30 복습을 위한 수정 시작 프로세스는 독립적으로 작업을 할 수도 있고 공유된 자원을 가지고 공동 작업을 할 수도 있다고 이전 장에서 언급했다. 이번에는 본격적으로

devdebin.tistory.com

 

자바에서의 모니터

  • 모든 자바 객체는 모니터를 가진다.
  • 여러 스레드가 객체의 임계 영역에 진입하려고 할 때 JVM은 모니터를 사용해 스레드 간 동기화를 제공한다.
  • 자바의 모니터는 상호 배제협력이라는 두 가지 동기화 기능을 제공하고 있으며 이를 위해 뮤텍스와 조건 변수를 사용한다.

 

상호 배제(Mutual Exclusion)

  • 객체가 가지고 있는 모니터 Lock을 통해 여러 스레드가 동시에 공유 자원에 접근하는 것을 막아 데이터의 일관성과 안전성을 보장하는 메커니즘
  • JVM은 synchronized 키워드를 사용해 뮤텍스 동기화를 암묵적으로 처리해주고 있으며 synchronized는 메서드나 코드 블록에 적용 가능
  • synchronized 블록은 해당 객체의 모니터를 획득할 수 있으며 모니터를 획득한 스레드만이 임계영역에 접근 가능하고 그 외 다른 스레드들은 차단되어 대기 상태가 된다.
  • synchronized 블록을 빠져 나오면 모니터 Lock이 해제되고 대기 중인 다른 스레드 중 하나가 락을 얻고 임계 영역에 진입하여 작업을 수행하는 식으로 상호배제가 보장된다.

 

협력(Cooperation)

  • 협력은 모니터의 Condition Variable (조건변수)를 통해 스레드 간 공동의 목표를 위해 상호협력으로 데이터의 일관성과 안전성을 보장하는 동기화 메커니즘이다.
  • 조건변수는 Object 클래스의 메서드인 wait(), notify(), notifyAll()과 함께 작동하며 특정 조건이 만족될 때까지 스레드를 대기시키는 기능을 제공한다.
  • 스레드가 특정 조건에 부합하지 않으면  wait() 메서드를 호출해 조건 변수의 대기 셋에 들어가 대기한다.
  • 다른 스레드가 특정 조건을 만족해서 notify() 또는 notifyAll() 메서드를 호출해 해당 조건 변수의 대기셋으로부터 스레드들을 깨워 실행시키게 된다.
  • 조건 변수를 통해 스레드 간 대기와 통지를 서로 조절하면서 경쟁 조건과 같은 문제를 방지할 수 있다.

보통 모니터는 여러개의 조건 변수를 가지지만 자바의 모니터에는 오직 한 개의 조건 변수만 가질 수 있다.

 

모니터 대기 구조

자바의 모니터 내부에는 EntrySet(진입셋)과 WaitSet(대기셋) 이라는 대기 자료 구조가 있다.

이들은 멀티 스레드 환경에서 스레드들 간의 상호 작용을 조절하는 데 사용한다.

 

EntrySet

  • EntrySet은 모니터의 Lock을 획득하기 위해 대기 중인 스레드를 모아 놓은 자료구조다.
  • 스레드가 Lock을 사용 중인 경우 그외 다른 스레드는 EntrySet에 들어간다.
  • EntrySet에 있는 스레드들은 Lock이 반납될때까지 기다리며 락이 반납되면 Entry Set 중 하나의 스레드가 락을 획득하고 임계 영역으로 진입하게 된다.

 

WaitSet

  • WaitSet은 모니터의 조건 변수와 함께 사용하는 자료구조이며 스레드들이 특정한 조건이 만족할 때까지 대기하고 있는 장소다.
  • 스레드는 WaitSet에 들어가 대기할 때 Lock을 해제한다.
  • 다른 스레드에 의해 깨어나게 되면 EntrySet으로 이동해서 다시 Lock을 획득할 수 있다.

 

 

조건변수

  • 조건변수를 통해 상호 협력하고 있는 두 스레드가 wait()과 notify() 메서드 실행 후에 하나의 모니터를 두고 두 스레드 모두 소유가 가능한 상황이 발생한다.
  • 하나는 대기중인 스레드, 하나는 깨우는 스레드로서 어떤 스레드가 모니터를 먼저 소유할 것인가에 따라 두 종류의 조건 변수로 나눌 수 있는데 Stop and Wait와 Signal and Continue가 있다.

 

Signal and Wait

  • 현재 모니터를 소유하고 있는 스레드가 wait() 을 실행하면 모니터 내부에서 자신을 일시 중단하고 Lock 을 해제한 후 Wait Set 에 들어간다.
  • 깨우는 스레드가 notify() or notifyAll() 명령을 실행하면 Wait Set 에 있는 대기 스레드 중 하나 또는 모든 스레드를 깨우고 깨우는 스레드는 Lock 을 해제하고 대기한다.
  • 대기에서 깨어난 스레드가 Lock 을 획득한 후 모든 작업을 마치고 Lock 을 해제하면 깨운 스레드가 Lock 을 획득한 후 계속 작업을 진행한다.
  • 대기 스레드와 깨운 스레드 사이에 다른 스레드가 모니터를 소유할 수 없도록 원자적 실행이 보장되어야 한다.

 

Signal and Continue

  • 현재 모니터를 소유하고 있는 스레드가 wait() 실행하면 모니터 내부에서 자신을 일시 중단하고 Lock 해제한  Wait Set 들어간다.
  • 깨우는 스레드가 notify() or notifyAll() 명령을 실행하면 Wait Set 있는 대기 스레드 하나 또는 모든 스레드를 깨운다. 이때  일어난 스레드들은 Entry Set 으로 이동한다.
  • 깨우는 스레드는 Lock 계속 유지하면서 모든 작업을 완료하고 Lock 해제하면 Entry Set 대기하고 있는 모든 스레드가 Lock 획득하기 위해 경쟁한다.
  • 자바에서는 이 조건 변수 형식을 취하고있다.
  • 개인적으로 이게 더 자연스러운 것 같다.

 

synchronized

앞에서 모든 자바 객체는 모니터를 가지고 있고, 모니터를 사용하기 위해서는 synchronized 키워드를 사용해야한다고 언급했다.

이에 대해 더 살펴보자.

 

  • synchronized는 명시적 락을 구현하는 것이 아닌 자바에 내장된 락으로 이를 암묵적인 락(Intrinsic Lock), 모니터락(Monitor Lock)이라고 한다.
  • synchronized은 동일한 모니터를 객체에 대해 오직 하나의 스레드만 임계영역에 접근할 수 있도록 보장하며 모니터의 조건 변수를 통해 스레드간 협력으로 동기화를 보장해준다.
  • synchronized가 적용된 한 개의 메서드만 호출해도 같은 모니터의 모든 synchronized 메서드까지 락에 잠기게 되어 락이 해제될 때까지는 접근이 안되는 특징을 가지고 있다.
  • 락은 스레드가 synchronized에 들어가기전에 자동 확보되며 정상적이든 비정상적이든 예외가 발생해서든 해당 블록을 벗어날 때까지 자동으로 해제된다.

 

동기화 방법

인스턴스 메서드 동기화

  • 인스턴스 단위로 모니터가 동작하며 동일한 인스턴스 안에서 synchronized 적용된 곳은 하나의 락을 공유
  • 인스턴스가 여러개일 경우 인스턴스별로 모니터 객체를 가지므로 스레드는 모니터 별로 락을 획득해서 동기화 영역에 진입하고 빠져 나올 락을 해제 있다.

 

public class MyClass {
    public synchronized void syncMethod1() {
          //동기화가 필요한 영역
   } 
    public synchronized void syncMethod2() {
         //동기화가 필요한 영역
    }
    
    public static void main(String[] args) {
    	//인스턴스 메서드에 관한 동기화에 대해서 서로 영향이 없다.
        MyClass m1 = new MyClass().syncMethod1(); 
        MyClass m2 = new MyClass().syncMethod1(); 
    }
}

 

정적 메서드 동기화

  • 클래스 단위로 모니터가 동작하며 synchronized 적용된 곳은 하나의 락을 공유
  • 인스턴스와는 별개의 모니터를 가지고 임계 영역을 동기화 하기 때문에 인스턴스 단위로 메서드를 호출할지라도 악은 클래스 단위로 스레드간 공유
  • 클래스는 메모리에 오직 하나만 존재하므로 하나의 모니터를 공유해서 동기화 하고자 사용 있다.

 

public class MyClass {
    public static synchronized void syncMethod1() {
        //동기화가 필요한 영역
    }
    public static synchronized void syncMethod2() {
        //동기화가 필요한 영역
    }

    public static void main(String[] args) {
    	//클래스 인스턴스는 메모리에 1개만 존재.
        MyClass.syncMethod1();
        MyClass.syncMethod1();
    }
}

 

인스턴스 블록 동기화

  • 인스턴스 단위로 모니터가 동작하며 synchronized 적용된 곳은 하나의 락을 공유
  • 모든 인스턴스가 모니터를 가지기 때문에 모니터를 여러 인스턴스로 구분해서 동기화를 구성할 있다.
  • 클래스의 인스턴스가 여러 개일 경우 인스턴스별로 모니터 객체를 가지며 스레드는 모니터 별로 락을 획득해서 synchronized 영역을 진입하고 빠져 나올 락을 해제 있다.

 

public class MyClass {

    public void syncMethod1() {
        synchronized(this){
            //동기화가 필요한 영역
        }
    }

    public void syncMethod2() {
        synchronized(this){
            //동기화가 필요한 영역
        }
    }

    public static void main(String[] args) {
    	//다른 인스턴스이므로 다른 모니터
        new MyClass().syncMethod1();
        new MyClass().syncMethod1();
    }
}

 

정적 블록 동기화

  • 클래스 단위로 모니터가 동작하며 synchronized 적용된 곳은 하나의 락을 공유
  • 모든 클래스가 모니터를 가지기 때문에 모니터를 여러 클래스로 구분해서 동기화를 구성할 있다.
  • 클래스 모니터가 여러개일 경우 스레드는 모니터 별로 락을 획득해서 synchronized 영역을 진입하고 빠져 나올 락을 해제 있다.

 

public class MyClass {

    public static void syncMethod1() {
        synchronized(MyClass.class){
            //동기화가 필요한 영역
        }
    }

    public static void syncMethod2() {
        synchronized(YourClass.class){
            //동기화가 필요한 영역
        }
    }

    public static void main(String[] args) {
    	//클래스 인스턴스는 메모리에 1개. 동일한 모니터
        MyClass.syncMethod1();
        MyClass.syncMethod1();
    }
}

 

그 밖의 synchronized에서 알아야할 내용

재진입성

  • 모니터 내에서 이미 synchronized 영역에 들어간 스레드가 다시 같은 모니터 영역으로 들어갈 있는데, 이를 "모니터 재진입"이라고 한다.
  • 재진입 가능하다는 것은 락의 획득이 호출 단위가 아닌 스레드 단위로 일어난다는 것을 의미하며 이미 락을 획득한 스레드는 같은 락을 얻기 위해 대기할 필요 없이 synchronized 블록을 만났을 같은 락을 확보하고 진입한다.

 

상속

  • 상속하게 되면 자식은 부모의 락과 동일한 락을 가지게 된다
  • 동기화 메서드에서 다른 동기화 메서드를 호출하는 경우 이미 (lock) 가지고 있는 스레드가 같은 락을 확보하고 재진입 데드락이 발생하지 않고 정상적으로 진행할 있게 된다.

 

public class MyClass {

    static class Parent {
        public synchronized void method() {
            System.out.println("Parent method");
        }
    }

    static class Child extends Parent {
        @Override
        public synchronized void method() {
            System.out.println("start super");
            super.method();
            System.out.println("finish super");
        }
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.method();
    }
}

 

가시성

  • synchronized 가시성을 지원
  • 가시성이란 스레드가 공유자원을 수정하거나 쓰기 작업을 했을 다른 스레드가 수정한 내용이 보이는 것이다.
  • cpu 캐시에 수정한 변수 값을 적지 않고 메모리에 직접 적는 것을 의미한다.

 

기타

  • sleep() 실행한 스레드는 동기화 영역에서 대기 중이더라도 획득한 락을 놓거나 해제하지 않는다.
  • synchronized 동기화 영역에 진입하지 못하고 대기 중인 스레드는 인터럽트 되지 않는다.
  • synchronized 동기화 영역에 진입하지 못하고 대기 중인 스레드가 다시 경쟁해서 모니터를 획득하는 것은 순서가 정해져 있지 않다.
  • wait(), notify(), notifyAll()은 모두 synchronized 블록 안에서만 사용해야 한다. 즉 모니터 락을 확보한 상태에서만 작동한다.

 

현대 애플리케이션은 여러 자바 애플리케이션 서버가 구동되므로 단일 jvm에서 작동하는 synchronized를 잘 사용하지 않는다. 

 

 

참고자료 

https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C-part1/dashboard

 

반응형

댓글