반응형
모니터란
모니터란 뮤텍스나 세마포어보다 더 고수준의 동기화 기법이다.
전공 수업에서는 세마포어를 편리하게 사용하기 위해 인터페이스를 제공한 것이 모니터라고 배웠었다.
부족하지만 수업을 듣고 정리해 놓은 글이 있다.
https://devdebin.tistory.com/16#%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4-1
자바에서의 모니터
- 모든 자바 객체는 모니터를 가진다.
- 여러 스레드가 객체의 임계 영역에 진입하려고 할 때 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를 잘 사용하지 않는다.
참고자료
반응형
댓글