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();
}
이 방식은 동기화 오버헤드를 최소화하며, 메서드 단위 동기화와 동일한 인스턴스 락을 사용한다.
정리
인스턴스 동기화: 객체 단위 격리가 필요한 경우. 다중 인스턴스 환경에서 병렬 처리 가능.
클래스 동기화: 전역 자원 보호가 필요한 경우. 클래스 로더당 단일 락으로 모든 인스턴스 통제.
혼합 사용: 두 메커니즘은 완전히 분리되어 있어 동시에 호출해도 차단되지 않는다.
클래스 동기화: 전역 자원 보호가 필요한 경우. 클래스 로더당 단일 락으로 모든 인스턴스 통제.
혼합 사용: 두 메커니즘은 완전히 분리되어 있어 동시에 호출해도 차단되지 않는다.