인스턴스 동기화와 클래스 동기화의 잠금 범위 분석

Java의 synchronized 키워드는 잠금 대상에 따라 동작 방식이 완전히 달라진다. 인스턴스 메서드와 정적 메서드에 각각 적용했을 때 어떤 차이가 있는지 살펴본다.

잠금 대상의 핵심 차이

다음 클래스를 통해 두 가지 동기화 방식을 비교한다:
public class Resource {
    // 인스턴스 잠금 - 각 객체마다 별도의 락
    public synchronized void instanceLockA() {}
    public synchronized void instanceLockB() {}
    
    // 클래스 잠금 - 전역 단일 락
    public static synchronized void classLockA() {}
    public static synchronized void classLockB() {}
}

동시 접근 가능성 분석

두 개의 인스턴스 r1, r2를 생성했을 때, 다음 조합들의 동시 실행 가능 여부를 검증한다:
조합동시 실행이유
r1.instanceLockA()r1.instanceLockB()불가동일 인스턴스의 락 공유
r1.instanceLockA()r2.instanceLockA()가능별도 인스턴스, 별도 락
r1.classLockA()r2.classLockB()불가클래스 레벨 단일 락
r1.instanceLockA()Resource.classLockA()가능락 대상이 완전히 분리됨
마지막 경우가 핵심이다. 인스턴스 동기화는 this를 잠그고, 정적 동기화는 Resource.class를 잠그므로 서로 간섭하지 않는다.

실제 동작 검증

CountDownLatch와 스레드 풀을 활용해 경쟁 상태를 재현한다:
import java.util.concurrent.*;

public class LockScopeDemo {
    private int counter;
    private static int staticCounter;
    
    // 인스턴스 메서드 동기화
    public synchronized void incrementInstance() {
        counter++;
        System.out.printf("[인스턴스] %s: counter=%d%n", 
            Thread.currentThread().getName(), counter);
    }
    
    // 정적 메서드 동기화
    public static synchronized void incrementStatic() {
        staticCounter++;
        System.out.printf("[클래스] %s: staticCounter=%d%n", 
            Thread.currentThread().getName(), staticCounter);
    }
    
    public static void main(String[] args) throws InterruptedException {
        final int THREAD_COUNT = 10;
        ExecutorService workers = Executors.newFixedThreadPool(THREAD_COUNT);
        
        CountDownLatch prepare = new CountDownLatch(THREAD_COUNT);
        CountDownLatch trigger = new CountDownLatch(1);
        CountDownLatch complete = new CountDownLatch(THREAD_COUNT);
        
        LockScopeDemo target = new LockScopeDemo();
        
        for (int n = 0; n < THREAD_COUNT; n++) {
            workers.submit(() -> {
                prepare.countDown();
                try {
                    trigger.await();
                    // 동일 스레드에서 두 락 모두 획득 가능함을 확인
                    target.incrementInstance();
                    incrementStatic();
                } catch (InterruptedException ignored) {}
                complete.countDown();
            });
        }
        
        prepare.await();
        System.out.println("=== 모든 스레드 준비 완료, 동시 시작 ===");
        trigger.countDown();
        
        complete.await();
        workers.shutdown();
    }
}
실행 결과에서 인스턴스 메서드와 정적 메서드의 출력이 서로 섞여 나오는 것을 확인할 수 있다. 이는 두 락이 독립적으로 동작함을 보여준다.

블록 동기화의 세밀한 제어

메서드 전체가 아닌 특정 구간만 보호해야 할 때는 블록 동기화가 유리하다:
public void optimizedAccess() {
    // 락 불필요한 작업
    prepareData();
    
    // 임계 구역만 보호
    synchronized(this) {
        modifySharedState();
    }
    
    // 락 불필요한 후처리
    cleanup();
}
이 방식은 동기화 오버헤드를 최소화하며, 메서드 단위 동기화와 동일한 인스턴스 락을 사용한다.

정리

인스턴스 동기화: 객체 단위 격리가 필요한 경우. 다중 인스턴스 환경에서 병렬 처리 가능.
클래스 동기화: 전역 자원 보호가 필요한 경우. 클래스 로더당 단일 락으로 모든 인스턴스 통제.
혼합 사용: 두 메커니즘은 완전히 분리되어 있어 동시에 호출해도 차단되지 않는다.

태그: java concurrency Synchronized thread-safety Static-Synchronization

7월 10일 16:26에 게시됨