자바에서 synchronized 키워드를 사용할 때, 이 키워드가 정확히 어떤 객체나 메모리 공간에 락을 거는지에 대해 헷갈렸다. 특히 synchronized 메서드나 synchronized 블록을 사용할 때, 이 락이 걸리는 대상이 단순히 메서드 자체인지, 혹은 메서드 내부에서 사용되는 변수들인지 명확하게 알지 못했다. 이에 대한 답을 찾기 위한 과정을 정리하였다.
자바에서 synchronized는 특정 메서드나 코드 블록이 실행될 때 특정 객체에 락을 건다. 이를 통해 여러 스레드가 동시에 해당 객체의 공유 자원에 접근하지 못하도록 상호 배제(Mutual Exclusion)를 보장한다. 즉, 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가 메서드나 변수 자체가 아닌, 객체 인스턴스에 락을 건다는 것이다.
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 메서드는 동시에 다른 스레드에서 호출될 수 없으며, 락을 획득한 스레드가 실행을 완료할 때까지 다른 스레드는 기다리게 된다.
결론적으로, synchronized는 메서드나 코드 블록 자체에 락을 거는 것이 아니라, 해당 메서드나 블록을 실행할 때 특정 객체에 락을 걸어 다른 스레드가 동시에 해당 객체에 접근하는 것을 막는다. 따라서 synchronized 키워드를 사용할 때 보호하고자 하는 공유 자원이 무엇인지를 명확히 파악하고, 그 자원을 안전하게 보호할 수 있도록 올바른 객체에 락을 걸어야 한다는 점을 알게 되었다.
이 과정을 통해, synchronized 키워드가 동기화된 메서드나 블록 내부에서 다루는 모든 변수에 대한 보호를 제공하는 것이 아니라, 락이 걸린 객체를 기준으로 상호 배제를 보장한다는 사실을 깨닫게 되었다.