OS

[ 동기화 #5 ] 모니터 in JAVA

qpmi1zm29 2023. 12. 27. 19:22
- 자바에서 모든 인스턴스는 1개의 모니터를 가지고 있으며, 각 모니터는 1개의 ConditionVariable만을 가질 수 있다.
- synchronized 키워드를 통해 동기화 메커니즘을 코드적으로 구현할 수 있다.
- 인스턴스 lock 뿐만 아니라 클래스 lock 또한 존재한다. 
- 인스턴스 lock 과 클래스 lock은 별개의 lock으로 공유되지 않는다.
- 자바는 기본적으로 signal and continue 방식을 지원한다.

 

자바에서 모니터 연산 ?

앞서 보았던 모니터 연산에서 메소드 명만 변경이 되었고 역할은 동일하다.

wait () : 스레드가 lock을 취득했으나, 코드 실행 조건에 맞지 않아 해당 스레드를 condition variable의 waiting queue를 통해 wait 상태로 전환한다.

notify() : 모니터에 존재하는 단일 condition variable의 waiting queue에 존재하는 스레드 중 하나를 깨운다.

nofityAll(): 모니터에 존재하는 단일 condition variable의 waiting queue에 존재하는 모든 스레드를 깨운다.

 

 

실제 어플리케이션 개발 시, 직접 wait이나 notify를 호출하는 것은 매우 위험하다.

public class Monitor {

    private int[] buffer = new int[5];
    private int index = 0;


    public synchronized void producer() {
        int item = index * 10 ;

        while ( index == 4 ) {
            wait();
        }

        buffer[index++] = item;

        notifyAll();  // producer와 consumer 모두 같은 CV를 사용해야 해서 모든 스레드를 깨워야 한다.

        
    }

    public void consumer() {
            int i = -1;

            synchronized ( this ) { // synchronized 블럭의 인자로 this를 사용해야 synchronized 메소드와 같은 lock을 공유할 수 있다.
                while ( index == 0 ) {
                    wait();
                }

                i = buffer[--index];

                notifyAll();
            }

    }
}

 

lock을 획득한 후 변경한 변수 값에 대해서 다음 lock을 획득한 대상에게 가시성을 보장하기 때문에, index 값을 조작하였을 경우 로직이 정상 동작 하게 된다.

 

위 코드 예시는 모두 인스턴스 lock을 사용한 예시인데, 클래스 lock을 사용하는 경우도 존재한다.

 

 

클래스 lock 사용 예시 1
    public static synchronized void producer() {
        int item = index * 10 ;

        while ( index == 4 ) {
            wait();
        }

        buffer[index++] = item;

        notifyAll();        
    }

 

synchronized 메소드에 static을 붙이면 클래스 lock을 생성하게 된다.

물론 static 클래스 변수를 다루는 것이 아니라면 인스턴스 lock을 주로 사용하지 않을까 싶다.

 

    public static void main ( String[] args ) {
        Monitor monitor1 = new Monitor();
        Monitor monitor2 = new Monitor();

        Runnable r1 = () -> {
            monitor1.producer();
        };

        Runnable r2 = () -> {
            monitor2.producer();
        };

        r1.run();
        r2.run();
        
    }

 

서로 다른 Monitor 인스턴스의 메소드를 실행시키지만, r1.run() 과 r2.run()는 producer()를 호출할 시, 동기화가 발생하게 된다.

 

 

클래스 lock 사용 예시 2

 

public void consumer() {
            int i = -1;

            B b = new B();
        
            synchronized ( Monitor.class ) { 
                while ( index == 0 ) {
                    wait();
                }

                i = buffer[--index];

                notifyAll();
            }

    }

 

첫번째 사용 예시와 같이 클래스 lock을 사용하여 서로 다른 인스턴스 이더라도 동기화가 발생하게 되는 코드이다.

 

여기서 만약에 synchronized 블록 인자로 b 인스턴스를 넣게되면 어떨까??

 

    public void consumer() {
            int i = -1;

            B b = new B();
        
            synchronized ( b ) { 
                while ( index == 0 ) {
                    wait();
                }

                i = buffer[--index];

                notifyAll();
            }

    }

 

이 경우 인스턴스 lock을 사용하는 예시인데, 메소드 호출 시 마다 매번 다른 B의 인스턴스를 사용하기 때문에, 하나의 Monitor 인스턴스에서 consumer() 를 여러 번 호출하여도 lock을 공유하지 않기 때문에 동기화가 되지 않는다. 

 

클래스 lock은 private 생성자와 static 메소드를 사용하여 싱글톤을 구현하는 예시에서도 사용된다.