synchronized는 Java에서 공유 자원에 대한 동시 접근을 제어하기 위한 동기화 메커니즘이다.
count++;
위 코드는 count를 읽고 1을 더하고 다시 사용하는 코드다.
→ 여러 스레드가 동시에 실행하면 값이 깨진다.
while (!flag) { }
한 스레드가 flag = true로 바꿔도 다른 스레드가 CPU 캐시 때문에 변경을 못 볼 수 있다.
상호배제는 여러 스레드가 동시에 같은 자원에 접근하지 못하도록 막는 것을 의미한다.
하나의 스레드가 특정 코드나 데이터를 사용하고 있을 때 다른 스레드는 그 영역에 들어올 수 없게 함으로써, 연산 중간에 값이 꼬이거나 데이터가 깨지는 경쟁 상태를 방지한다.
가시성 보장은 한 스레드가 변경한 값이 다른 스레드에서도 즉시 보이도록 보장하는 것을 의미한다.
CPU 캐시나 컴파일러 최적화로 인해 변경된 값이 다른 스레드에 보이지 않는 문제가 발생할 수 있는데, synchronized는 락을 획득하고 해제하는 과정에서 변경된 값을 메인 메모리에 반영하고 다시 읽게 해서 이런 문제를 해결한다.
ex)
스레드 A, 스레드 B가 있는데 둘 다 아래의 같은 코드를 실행하려고 한다.
synchronized(obj) {
// 임계 구역
}
스레드는 저 코드를 만나면 obj의 Monitor가 비어 있는지 확인한다.
(Monitor = 객체에 딸린 출입 열쇠)
(스레드 A가 먼저 도착했다고 가정)
먼저 JVM이 obj의 Monitor 상태를 확인한다. 근데 A가 B보다 먼저왔으니 아무도 안 쓰고 있을거니까 스레드 A가 Monitor를 획득한다. 그리고 스레드 A가 임계 구역을 실행한다.
(obj의 Monitor → 스레드 A가 소유 중)
이때 스레드 B가 도착하면 JVM이 obj의 Monitor를 확인하는데, 이미 스레드 A가 가지고 있으니까 스레드 B는 바로 멈춘다. 그대로 CPU를 사용하지 않고 대기한다.
스레드 A가 obj의 Monitor를 반납하고 변경된 값들을 메인 메모리에 반영한다. 그리고 대기 중인 스레드 하나를 깨운다.
이제 스레드 B는 Monitor가 비었음을 확인하고 Monitor 획득해서 실행한다.
모든 Java 객체는 메모리에 객체 헤더를 가지고 있고, 그 안에 Mark Word라는 영역이 있다.
Mark Word는 다음 정보들을 저장한다.
synchronized는 이 Mark Word를 활용하여 락을 구현한다.
public synchronized void increase() {
count++;
}
인스턴스 메서드 동기화는 메서드 호출 시 해당 객체(this)의 모니터 락을 획득해서 실행하는 방식이다.
같은 객체의 synchronized 인스턴스 메서드들은 하나의 락을 공유하므로 동시에 실행될 수 없지만, 서로 다른 객체의 메서드는 각각 다른 락을 사용하므로 병렬 실행이 가능하다.
public static synchronized void increase() {
count++;
}
static 메서드 동기화는 인스턴스가 아니라 클래스 자체의 모니터 락을 획득하여 실행하는 방식이다.
클래스 객체는 JVM 내에 하나만 존재하므로, 어떤 인스턴스에서 호출하더라도 해당 클래스의 static synchronized 메서드는 전체 애플리케이션에서 동시에 하나만 실행된다.
synchronized(lockObject) {
count++;
}
블록 동기화는 synchronized 블록에 지정한 특정 객체의 모니터 락을 획득하는 방식으로, 개발자가 직접 락 대상을 선택할 수 있다.
필요한 코드 영역만 동기화할 수 있어 불필요한 락 범위를 줄일 수 있고, 성능과 설계 측면에서 가장 유연하고 권장되는 방식이다.
재진입 가능이란 같은 스레드가 이미 획득한 락을 다시 획득할 수 있다는 의미다.
public synchronized void outer() {
System.out.println("outer");
inner(); // 같은 락을 다시 획득 시도
}
public synchronized void inner() {
System.out.println("inner"); // 정상 실행됨 (데드락 X)
}
JVM은 락을 획득한 스레드를 기록하고, 재진입 횟수를 카운트한다.
만약 재진입이 불가능하다면 자기 자신이 가진 락을 기다리는 자기 데드락이 발생한다.
synchronized는 예외가 발생해도 자동으로 락을 해제한다.
synchronized(obj) {
// 작업 수행
throw new RuntimeException("에러!");
// 이 코드는 실행 안 됨
} // 예외가 발생해도 여기서 자동으로 락 해제됨
이는 try-finally 구조가 내부적으로 보장되기 때문이다. 따라서 예외 때문에 락이 영구적으로 잠기는 일은 없다.
synchronized는 락을 해제할 때 어떤 스레드가 먼저 실행될지 보장하지 않고, 오래 기다린 스레드가 계속 밀리는 기아 현상이 발생할 수 있다.
락을 기다리는 동안 스레드는 BLOCKED 상태가 되고, 이 상태에서는 interrupt()를 호출해도 즉시 깨어나지 못하고 락이 풀릴 때까지 대기한다.
synchronized는 락 획득에 실패했을 때 대안 동작을 선택할 수 없고, 락을 얻을 때까지 무조건 기다려야 한다.