관리 메뉴

백엔드 엔지니어 이재혁

[Java] synchronized 본문

Java

[Java] synchronized

alex00728 2025. 5. 22. 17:08

Java에서 synchronized 메서드(혹은 코드 블럭)는 다음과 같이 동작한다.

Java 객체에서 기본으로 제공하는 락은 "모니터 락"이라고 부른다.

  1. 객체당 하나의 모니터 락만 존재
  2. 한 스레드가 synchronized 메서드를 실행하면, 해당 객체의 락을 획득
  3. 다른 스레드는 같은 객체의 어떤 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()` 프레임을 갖게 되고, 각 프레임에 개별적인 지역 변수들이 자리잡게 된다. 애초에 공유하는 자원이 아닌 것이다.

count() 프레임이 스레드의 스택에 각각 생성됨

 

 

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

 

위 예제들은 primitive type인 int 값을 참조해서 값과 변수의 위치가 동일해 이해하기 편하다. 

 

 

더 정확하게 이해해보자

`Object localValue = new Object();` 와 같이 primitive type이 아닌 경우에도 `count()` 메서드 안에서 새로운 객체를 생성해 서로 다른 객체를 가르키는 상황으로 이해하면 된다.

Object localValue = new Ojbect();

 

위에서는 각 메서드에서 새로운 객체를 만드는 방식이지만, 이와 다르게 어떤 다른 방법으로 각 지역변수가 하나의 공유 객체를 가르키면 동시성 문제가 발생할 수 있다. (싱글톤 객체라던지)

 

 

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