synchronized는 자바에서 동시성 문제를 해결하기 위해 사용할 수 있는 간단한 동기화 기법입니다.
단순히 키워드를 추가하거나 블록을 만들면 사용할 수 있기 때문입니다.
다만, 사용법과 달리 synchronized에 적용된 이론이나 동작 과정은 생각보다 복잡합니다.
이를 제대로 이해하기 위해 synchronized와 관련된 내용을 분석하고 정리했습니다.
Mutex(Mutual Exclusion)으로, 상호 배제라는 의미를 가지고 있습니다.
여러 스레드가 동시에 임계 영역에서 공유되는 리소스에 접근해 동시성 문제가 발생하는 것을 방지하는 동기화 기법 중에 하나입니다.
상호 배제라는 의미처럼, 하나의 스레드가 플래그(= 락)을 획득해 임계 영역에서 작업을 수행하고 있으면 다른 스레드는 접근하지 못합니다.
모니터(Monitor) 또한 동기화 기법 중 하나로, 뮤텍스나 세마포어보다 더 고수준(= 더욱 추상화 되어 있음)의 동기화 기법입니다.
다음과 같은 방식으로 동시성 문제를 해결합니다.
모니터에서 스레드의 역할은 대기 중인 스레드(wait)와 락을 가진 상태로 대기 중인 스레드를 깨우는 스레드(signal)로 구분할 수 있습니다.
(앞으로 각각 wait 스레드, signal 스레드라고 명칭하겠습니다.)
이 때, 어떤 종류의 스레드가 먼저 모니터를 가질지에 따라 조건 변수의 종류를 구분할 수 있습니다.
모니터 조건 변수 종류와 동작 방식은 다음과 같습니다.
자바에서는 모든 객체가 모니터를 가지게 됩니다.(Intrinsic Locks 혹은 Monitor Locks, Monitor라고 표현합니다.)
모든 객체는 Object 객체를 상속하므로, 모니터는 Object에 의해 관리됩니다.
이는 Object 클래스 문서에서 확인할 수 있습니다.
모니터와 관련된 notify(), notifyAll(), wait() 메서드를 확인할 수 있습니다.
메서드의 설명에 더 명확하게 확인할 수 있습니다.
자바에서의 모니터는 다음과 같이 구성됩니다.
자바에서 모니터 조건 변수로 Signal and Wait, Signal and Continue 중 Signal and Continue를 사용합니다.
또한, OS 레벨의 모니터 조건 변수는 여러 개를 가질 수 있지만 자바에서의 모니터는 단 하나의 조건 변수를 가지고 있습니다.
이로 인해 OS 레벨의 모니터보다는 유연하지 못하지만, Object.notifyAll()로 Entry Set에서 대기하고 있는 모든 스레드를 깨울 수 있습니다.
대신 유연하게 스레드를 관리하지 못한다는 단점이 있습니다.
유연하게 스레드를 관리해야 할 경우, synchronized 대신 Lock을 활용해 처리할 수 있습니다.
자바의 synchronized는 가장 간단하게 동기화 기법을 적용할 수 있는 키워드입니다.
암시적으로 모니터를 통해 관리하게 됩니다.
synchronized 종류는 다음과 같이 크게 두 가지로 나뉩니다.
synchronized method는 메서드 시그니처에 synchronized 키워드를 추가하는 방식입니다.
synchronized instance method는 일반 메서드에 synchronized 키워드를 추가하는 방식입니다.
public synchronized void method() {
// 임계 영역
}
이 때 사용되는 락은 해당 메서드가 실행되는 인스턴스 자기 자신을 의미합니다.
즉, this에서 락을 가져옵니다.
synchronized static method는 static 메서드 시그니처에 synchronized 키워드를 추가하는 방식입니다.
public static synchronized void method() {
// 임계 영역
}
이 때 사용되는 락은 해당 메서드가 위치한 클래스를 의미합니다.
즉 클래스에서 락을 가져옵니다.
public synchronized void method1() {
// 임계 영역
}
public static synchronized void method2() {
// 임계 영역
}
이 경우 method1과 method2는 서로 다른 모니터를 가지게 됩니다.
synchronized method는 bytecodeInterpreter에서 메서드를 분석하고 실행할 때, synchronized 키워드 여부를 확인하고 락을 획득합니다.
이 과정에서 biased lock(= 원자적 연산을 수행하지 않는 락)을 사용할 수 있는지 여부를 판단하고 사용할 수 있다면 biased lock을 사용하고, 아닐 경우 synchronized block과 동일한 방식으로 락을 사용합니다.
synchronized block은 코드 블록에 synchronizd 키워드를 통해 임계 영역을 설정하는 방법입니다.
synchronized instance block은 코드 블럭에 인스턴스를 지정하는 방식입니다.
Object lock = new Object();
public void method() {
synchronized (lock) {
// 임계 영역
}
}
이 때 사용되는 락은 코드 블록에 지정한 인스턴스를 의미합니다.
synchronized static block은 코드 블록을 통해 클래스를 지정하는 방식입니다.
public void method() {
synchronized (Class.class) {
// 임계 영역
}
}
이 때 사용되는 락은 코드 블록에 지정한 위치한 클래스를 의미합니다.
Object lock = new Object();
public void method1() {
synchronized (lock) {
// 임계 영역
}
}
public void method2() {
synchronized (Class.class) {
// 임계 영역
}
}
이 경우 method1과 method2는 서로 다른 모니터를 가지게 됩니다.
synchronized block의 경우 모니터에서 락을 획득하고 해제하는 과정이 추가됩니다.
public class Main {
public void method1() {
synchronized (this) {
// 임계 영역
}
}
}
위와 같은 예제 코드를 컴파일한 뒤 바이트 코드를 출력하면 다음과 같은 내용을 확인할 수 있습니다.
monitorenter의 경우 모니터에 진입해 락을 획득하며, monitorexit의 경우 락을 해제하고 모니터에서 빠져나옵니다.
컴파일을 했을 때 추가되는 만큼 자바 컴파일러가 이를 추가하며, 이와 관련된 내용은 interpreterRuntime.cpp에서 확인할 수 있습니다.
각각 별도의 모니터를 가지고 있으므로, 다음과 같이 총 4개의 락을 가질 수 있습니다.
이미 모니터 영역에 들어가 락을 획득했다면, 이후 동일한 모니터 영역에 들어갈 때 락 획득 절차를 생략하고 바로 진입이 가능합니다.
이를 모니터 재진입이라고도 표현합니다.
class Super {
public synchronized void method1() {
// 임계 영역
}
}
class Sub extends Super {
public synchronized void method2() {
super.method1();
}
}
Sub sub = new Sub();
sub.method2();
자바는 모니터를 Object를 통해 관리합니다.
그러므로 Object -> Super -> Sub의 계층 구조를 가지고 있는 경우, 결과적으로는 모두 동일한 모니터를 갖게 됩니다.
하지만 코드 상으로는 두 개의 synchronized가 분리되었으므로, 모니터 진입 시점은 Sub 한 번, Super 한 번이 됩니다.
이 경우 Sub에서 이미 모니터에 진입해 락을 획득했으므로, Super에서 synchronized 영역에 진입할 경우 락 획득 시도를 생략하고 즉시 접근이 가능합니다.
결과적으로는 T1은 작업을 모두 마쳤고, T2는 새롭게 임계 영역에 도달했습니다.
이와 같이 모니터는 기본적으로 상호 배제 기반으로 동작합니다.
이렇게 wait()를 호출해 Wait Set에 대기하는 스레드는 이후 다른 스레드가 notify() 혹은 notifyAll()을 호출하기 전 까지는 깨어나지 않습니다.
그러므로 지금 과정까지는 상호 협력과 상호 배제는 별도로 동작이 가능합니다.
시간이 경과해 위 그림과 같은 상황이라고 가정합니다.
synchronized의 특성 중 하나는 비공정성이기 때문에 T2가 T3보다 오래 기다렸지만 T3이 먼저 실행될 수 있으므로, T3이 임계 영역에 도달했다고 처리했습니다.
(Entry Set과 Wait Set에서 다음 스레드를 선택하는 기준은 OS 스케줄러에 의해 결정됩니다.)
이러한 과정을 통해 synchronized가 동기화 기법을 적용해 스레드를 관리합니다.
Object docs (java 8)
Intrinsic Locks and Synchronization
bytecodeInterpreter.cpp (openjdk)
interpreterRuntime.cpp (openjdk)
monitorenter, monitorexit (java se 6)