synchronized가 어떤 락을 거는지에 대한 탐구

한봉훈·2024년 10월 7일

자바에서 synchronized 키워드를 사용할 때, 이 키워드가 정확히 어떤 객체나 메모리 공간에 락을 거는지에 대해 헷갈렸다. 특히 synchronized 메서드synchronized 블록을 사용할 때, 이 락이 걸리는 대상이 단순히 메서드 자체인지, 혹은 메서드 내부에서 사용되는 변수들인지 명확하게 알지 못했다. 이에 대한 답을 찾기 위한 과정을 정리하였다.

1. synchronized가 락을 거는 대상은?

자바에서 synchronized는 특정 메서드나 코드 블록이 실행될 때 특정 객체에 락을 건다. 이를 통해 여러 스레드가 동시에 해당 객체의 공유 자원에 접근하지 못하도록 상호 배제(Mutual Exclusion)를 보장한다. 즉, synchronized는 메서드 자체에 락을 거는 것이 아니며, 그 메서드가 접근하는 객체에 대해 락을 거는 것이다.

  • 인스턴스 메서드에 synchronized를 사용할 경우, 해당 인스턴스 객체(this)에 락을 건다.
  • 정적 메서드에 synchronized를 사용할 경우, 클래스 객체 자체에 락을 건다.

2. 실험: 인스턴스 메서드에 synchronized 적용

class SharedResource {
    private int sharedValue = 0;

    // synchronized 키워드로 메서드 동기화
    public synchronized void increment() {
        sharedValue++;
    }

    public synchronized int getValue() {
        return sharedValue;
    }

    public static void main(String[] args) throws InterruptedException {
        SharedResource resource = new SharedResource();
        
        // 3개의 스레드가 동시에 increment 호출
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        // 스레드 실행
        t1.start();
        t2.start();
        t3.start();

        // 모든 스레드가 끝날 때까지 대기
        t1.join();
        t2.join();
        t3.join();

        // 최종 sharedValue 값 출력
        System.out.println("Final sharedValue: " + resource.getValue()); // 기대값 3000
    }
}

메서드를 호출하려고 하면, 한 스레드가 락을 획득한 동안 다른 스레드는 대기하게 된다.

하지만 다른 객체 인스턴스를 생성한 경우, 해당 객체는 서로 독립적인 락을 가지고 있기 때문에 동시 실행이 가능하다. 여기서 확인할 수 있는 것은 synchronized가 메서드나 변수 자체가 아닌, 객체 인스턴스에 락을 건다는 것이다.

3. 실험: synchronized 블록을 사용한 동기화

class SharedResource {
    private int sharedValue = 0;

    public void increment() {
        synchronized(this) { // this 객체에 락을 건다
            sharedValue++;
        }
    }

    public int getValue() {
        synchronized(this) {
            return sharedValue;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SharedResource resource = new SharedResource();
        
        // 3개의 스레드가 동시에 increment 호출
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });

        // 스레드 실행
        t1.start();
        t2.start();
        t3.start();

        // 모든 스레드가 끝날 때까지 대기
        t1.join();
        t2.join();
        t3.join();

        // 최종 sharedValue 값 출력
        System.out.println("Final sharedValue: " + resource.getValue()); // 기대값 3000
    }
}

위 코드에서 synchronized 블록은 this 객체에만 락을 건다. 즉, increment와 getValue 메서드는 동시에 다른 스레드에서 호출될 수 없으며, 락을 획득한 스레드가 실행을 완료할 때까지 다른 스레드는 기다리게 된다.

4. 정리: synchronized가 걸리는 대상은 무엇인가?

결론적으로, synchronized는 메서드나 코드 블록 자체에 락을 거는 것이 아니라, 해당 메서드나 블록을 실행할 때 특정 객체에 락을 걸어 다른 스레드가 동시에 해당 객체에 접근하는 것을 막는다. 따라서 synchronized 키워드를 사용할 때 보호하고자 하는 공유 자원이 무엇인지를 명확히 파악하고, 그 자원을 안전하게 보호할 수 있도록 올바른 객체에 락을 걸어야 한다는 점을 알게 되었다.

이 과정을 통해, synchronized 키워드가 동기화된 메서드나 블록 내부에서 다루는 모든 변수에 대한 보호를 제공하는 것이 아니라, 락이 걸린 객체를 기준으로 상호 배제를 보장한다는 사실을 깨닫게 되었다.

profile
백엔드 기록

0개의 댓글