백엔드 엔지니어 이재혁
[Java] synchronized 본문
Java에서 synchronized 메서드(혹은 코드 블럭)는 다음과 같이 동작한다.
Java 객체에서 기본으로 제공하는 락은 "모니터 락"이라고 부른다.
- 객체당 하나의 모니터 락만 존재
- 한 스레드가 synchronized 메서드를 실행하면, 해당 객체의 락을 획득
- 다른 스레드는 같은 객체의 어떤 synchronized 메서드도 실행할 수 없다.
아래와 같이 하나의 클래스 안에 다수의 메스드에 대해서 synchronized 키워드가 붙어있다면, 각 메서드에 락이 걸리는 것이 아니라, 객체 단위로 락이 걸리게 된다.
public class BankAccountV2 implements BankAccount{
private int balance;
public BankAccountV2(int initialBalance) {
this.balance = initialBalance;
}
@Override
public synchronized boolean withdraw(int amount) {
log("거래 시작" + getClass().getSimpleName());
// 잔고가 출금액보다 적으면, 진행하면 안됨
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
// 잔고가 출금액보다 많으면, 진행
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금에 걸리는 시간으로 가정
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public synchronized int getBalance() {
return balance;
}
}
락을 획득하지 못해 대기 중인 스레드는 BLOCKED 상태가 된다.
`[ main] t2 state: BLOCKED`
이 때, BLOCKED 상태의 스레드는 인터럽트가 통하지 않는다!
물론, 락이 걸려도 `synchronized` 키워드가 붙지 않은 메서드는 평소처럼 바로바로 실행이 가능하다.
참고로 synchronized를 사용해서 동시성 문제를 해결하면, 메모리 가시성 문제 없어진다. 그런 경우에는 `volatile` 키워드를 사용하지 않아도 된다.
synchronized 남발 주의
동시성 문제는 공유 자원에 동시에 접근할 때 발생할 수 있다.
아래 클래스에서 동시성 문제가 발생할 수 있을까?
class MyCounter {
public void count() {
int localValue = 0;
for (int i = 0; i < 1000; i++) {
localValue = localValue + 1;
}
log("결과: " + localValue);
}
}
발생하지 않는다.
처음에는 논리적으로 생각해봤을 때 각각의 스레드가 각자 따로 `count()` 메서드를 실행하니 그 안에 있는 `localValue`도 각자 가지고 있을 것이라고 생각을 했다.
그런데 그것보다는 Java의 Stack과 Heap을 떠올려보면 더욱 명쾌한 논리로 설명할 수 있다.
`count()` 메서드를 다수의 스레드에서 호출했다고 생각해보자. 이 때, 각 스레드의 스택에 `count()` 프레임을 얹게 된다. 스레드마다 개별적인 `count()` 프레임을 갖게 되고, 각 프레임에 개별적인 지역 변수들이 자리잡게 된다. 애초에 공유하는 자원이 아닌 것이다.

정말로 문제가 될 때는 아래와 같이 각 스레드의 개별적인 프레임에서 하나의 인스턴스(Heap 영역에 있는 공유 변수)를 동시에 접근할 때 문제가 된다. 이번에는 counter 인스턴스의 멤버 변수 `count`에 동시에 접근하는 상황.

위 예제들은 primitive type인 int 값을 참조해서 값과 변수의 위치가 동일해 이해하기 편하다.
더 정확하게 이해해보자
`Object localValue = new Object();` 와 같이 primitive type이 아닌 경우에도 `count()` 메서드 안에서 새로운 객체를 생성해 서로 다른 객체를 가르키는 상황으로 이해하면 된다.

위에서는 각 메서드에서 새로운 객체를 만드는 방식이지만, 이와 다르게 어떤 다른 방법으로 각 지역변수가 하나의 공유 객체를 가르키면 동시성 문제가 발생할 수 있다. (싱글톤 객체라던지)
Heap 영역에 있는 모든 공유 변수는 동시성 문제가 발생?
여러 스레드가 공유 자원에 접근하는 것 자체는 사실 문제가 되지 않는다. 진짜 문제는 공유 자원을 사용하는 중간에 다 른 스레드가 공유 자원의 값을 변경해버리기 때문에 발생한다. 결국 변경이 문제가 되는 것이다.
아래와 같이 `final` 키워드로 변수가 바뀌지 못하게 만들어버린다면 언제든지 누구나 같은 값을 보게 되므로 동시성 문제가 발생하지 않는다.
public class Immutable {
private final int value;
public Immutable(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Synchronized 단점
- 무한 대기: BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기한다.
- 특정 시간까지만 대기하는 타임아웃X
- 중간에 인터럽트X
- 공정성: 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없다.
- 최악의 경우 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있다.
이런 단점들을 해결하기 위해 Java는 concurrent라는 패키지를 통해 더 세밀한 동시성 제어 방법을 제공한다. (앞으로 배울 내용)
'Java' 카테고리의 다른 글
| [Java] 생산자 소비자 문제 (0) | 2025.05.26 |
|---|---|
| [Java] concurrent.locks (0) | 2025.05.23 |
| [Java] 메모리 가시성 (0) | 2025.05.22 |
| [Java] Runnable (0) | 2025.05.19 |
| [Java] Thread (0) | 2025.05.19 |