본문 바로가기
Old Posts/Java

[Java] synchronized 키워드 -고유락(Intrinsic Lock)

by A6K 2022. 4. 7.

자바는 멀티스레드 환경에서 동기화를 지원하기 위해 가장 기초적인 장치인 '고유락(Intrinsic Lock)'을 지원한다.(고유락은 모니터락(Monitor lock) 혹은 모니터(monitor)라고 부르기도 한다) 개발자는 synchronized 키워드를 이용해서 특정 객체의 고유락을 사용해 여러 스레드를 동기화 시킬 수 있다.

Java의 synchronized

자바의 synchronized 블럭은 다음과 같이 생겼다.

synchronized(obj) {
  // critical section
}

synchronized 블럭은 객체를 필요로 한다. 동일한 객체에 대해서 synchronized 블록을 사용하는 두 스레드는 한 번에 하나의 스레드만 synchronized 블록 내부로 들어갈 수 있다. 자바에서 제공하는 가장 기본적인 '상호배제(Mutual Exclusion)' 장치다.

synchronized 블럭을 사용하면 지정한 객체의 고유락(Intrinsic Lock)을 잡게 된다. 고유락은 한 번에 하나의 스레드만 잡을 수 있도록 JVM이 지원하며, 나머지는 대기한다. 블럭이 끝나면 JVM은 대기하던 스레드들을 한번에 깨우게 되고, 다시 경쟁을 통하 하나의 스레드만 고유락을 잡게 되어 synchronized 블럭 안쪽으로 들어가게 된다. 고유락은 블럭이 정상적으로 종료되는 경우 뿐 아니라 예외가 발생해서 상위로 throw 되는 경우에도 릴리즈 된다.

메서드에도 synchronized를 붙일 수 있다. 예를 들어

public class Test {
    private int value1 = 0;
    
    private int value2 = 0;
    
    public synchronized void increment() {
        this.value1++;
        this.value2++;
    }
}

increment() 메서드 앞에 synchronized 키워드가 붙은걸 볼 수 있다. 이 경우 synchronized 키워드는 this 객체의 고유락을 사용한다. 즉,

public class Test {
    private int value1 = 0;
    private int value2 = 0;
    
    public void increment() {
        synchronized(this) {
            this.value1++;
            this.value2++;
        }
    }
}

이 경우와 비슷하다고 생각하면 된다.

static 메서드에도 synchronized 키워드를 붙일 수 있다. static 메서드의 경우 기반이 되는 객체가 없기 때문에 Class 오브젝트의 고유락을 잡게 된다. 따라서 static 메서드에 붙은 synchronized 키워드는 그 클래스의 객체에 걸리는 고유락과 충돌하지 않는다.

public class Test {
    private int value = 0;
    
    // Compile Error
    public synchronized Test() {
    
    }
}

생성자에는 synchronized 키워드를 붙일 수 없다. 대신 생성자의 바디 부분에 synchronized 키워드를 넣어서 필요한 경우 동기화를 할 수는 있다.

public void method() {
  synchronized (this) {
  
    // sleep은 락을 놓지 않음
    Thread.sleep(1000);
  }
}

synchronized 블럭에 들어간 스레드가 내부에서 Thread.sleep()을 호출하더라도 잡았던 고유락이 해제되지는 않는다. 따라서 synchronized 블럭에 들어가기 위해 객체의 고유락을 기다리던 다른 스레드는 계속 기다려야한다. 이 때문에 우선순위가 낮은 스레드가 우선순위가 높은 스레드를 블럭시키는 우선순위 역전 현상이 발생하기도 한다.

synchronized 블럭에 대기중인 스레드는 인터럽트 되지 않는다. 인터럽트가 필요하다면 ReentrantLock 같은 명시적인 락을 사용해야 한다.

synchronized 블럭에 들어가기 위해 경쟁하는 스레드에는 우선순위가 없다. 따라서 고유락을 잡고 있던 스레드가 락을 릴리즈하는 순간 대기중이던 스레드가 경쟁하고 먼저 잡는 스레드가 우선 실행하게 된다. 따라서 기아상태(Starvation)가 발생할 수 있지만 가능성은 낮다.

synchronized 블럭에서 사용하는 고유락은 Reentrant 하다.

Reentrancy

스레드는 다른 스레드가 락을 가지고 있을 때, 그 락을 요청하면 기다려야한다. 하지만 스레드 자신이 이미 가지고 있는 락을 다른 코드패스를 통해서 다시 요청하는 경우가 있다. Reentrant 한 락은 내가 잡고 있는 락을 다시 요청한 경우 여러번 허용된다.

public static synchronized int fact(int n) {
    if (n <= 1)
        return n;
    else 
        return fact(n-1) * n;
}

synchronized 메서드를 재귀적으로 호출하면서 여러번 고유락을 요청하지만 문제없이 진행된다.

Reentrant 하지 않은 락이라면 내가 가지고 있는 락을 다시 획득하지 못하고, Self Deadlock에 빠질 수 있다.


가시성(Visibility)

synchronized 키워드는 위에서 설명했던 상호배제(Mutual Exclusion, Mutex)말고도 가시성을 제공한다. 가시성(Visibility)이란 한 스레드가 수정한 내용이 다른 스레드에서 보이도록 함을 의미한다.

예를 들어

public class Test {
  private int value = 0;
  
  public int getValue() {
    return this.value;
  }
  
  public void setValue(int value) {
    this.value = value;
  }
}

이런 클래스가 있다고 하자. 간단한 setter와 getter로 구성된 동기화 장치가 없는 클래스다. 프로세서1에서 setValue() 메서드를 실행한 다음 시간상 겹치지 않게 프로세서2에서 getValue()를 한 경우, 프로세서1에서 입력한 값이 안 보일 수 있다.

동기화 장치가 없다면 변수의 값을 바꾸는 작업은 메모리까지 값을 쓰지 않을 수 있다. 이 경우 다른 프로세서에서는 스테일 값(stale value)을 볼 수 있게 된다.

또, 컴파일 시 동기화 장치가 없는 경우 인스트럭션의 순서를 맘대로 바꿀 수 있다. 즉, 코딩되어 있는 순서로 값의 수정이 발생하지 않을 수 있다. 가시성에 대해서는 별도의 포스트를 할애해서 설명하도록 하겠다.


synchronized와 성능

synchronized는 상호배제를 구현해준다. 따라서 한번에 하나의 스레드만 synchronized 블럭으로 들어갈 수 있다. 만약 synchronized 블럭을 지나치게 큰 덩어리로 잡았다면 한 번에 하나의 스레드만 실행될 수 있기 때문에 멀티 스레드의 장점을 누릴 수 없다.

반대로 지나치게 synchronized 블럭을 세분화해놓으면 고유락을 너무 빈번하게 잡았다 놨다하게 된다. 고유락을 잡는 행위는 자바 코드에서는 synchronized 키워드 하나만 붙이면 되지만 JVM 내부에서는 무시할 수 없는 오버헤드가 발생한다. 따라서 빈번하게 락을 잡았다 놓으면 오히려 또 성능이 저하될 수 있다.

따라서 적당한 수준의 synchronized 블럭을 구성하는게 중요하다.

댓글