Java에서 임계구역을 보호(상호 배제, mutual exclusion)하기 위하여 대표적으로 두가지 락 매커니즘을 사용할 수 있다.
synchronized
public class SynchronizedExample {
private int counter = 0;
// synchronized 메서드 → this 객체의 모니터 락을 사용
public synchronized void methodWithKeyword() {
...
}
// synchronized 블록 → 동일하게 this 객체의 모니터 락을 사용
public void methodWithBlock() {
synchronized (this) {
...
}
}
}
synchronized 키워드를 사용하면 객체의 모니터 Lock을 사용할 수 있다. JVM이 이 Lock의 획득 및 해제를 자동으로 관리하며, 예외 등의 상황으로 인해 임계 구역을 벗어나면 무조건 락이 해제된다.
때문에 개발자는 가독성 있는 코드와 함께 안전하게 락을 해제 (정확하게는 자동으로 해제되므로) 할 수 있다.
단점은, synchronized 키워드를 통해서는 Timeout이나 Interrupt를 일으킬 수 없다. 때문에 무한 대기 문제가 생길 수 있어서 Deadlock 위험이 존재한다. 또한, JVM이 임계구역에 진입할 스레드를 알아서 고르는데, 이는 요청 순서대로 진입하는 것을 보장하지 않는다. 따라서 특정 스레드는 Starvation(기아) 상태에 빠질 수 있다.
ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
// ReentrantLock → 명시적으로 락 지정
public void methodWithLock() {
lock.lock();
try {
...
} finally {
lock.unlock();
}
}
}
ReentrantLock는 개발자가 명시적으로 선언하는 라이브러리 레벨의 Lock이다. 개발자는 Lock의 획득과 해제를 신경써서 코드를 작성해야 한다.
ReentrantLock은 synchronized 키워드 방식보다 더욱 유연한 Lock 관리가 가능하도록 한다. Timeout, Interrupt를 설정할 수 있으며 (Deadlock 회피), 설정을 통해 락 획득을 요청 기반 FIFO 방식을 통해 공정하게 할 수도 있다.
그러나 개발자가 Lock 해제를 직접 해야 하기 때문에, 예외 처리에 신경써야 하며, finally 블록 누락 시 논리적인 임계 구역은 벗어났는데 Lock을 계속 물고 있는 상황이 발생할 수 있다. 따라서 이를 신경쓰서 코드들을 작성하기 때문에 synchronized 키워드 방식에 비해 가독성이 떨어지는 문제가 있다.
Lock Reentrancy
다음 코드를 보고 실행 흐름을 파악해보자.
import java.util.concurrent.locks.ReentrantLock;
public class ReentrancyExample {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("methodA 진입");
methodB();
System.out.println("methodA 종료");
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 같은 lock 객체를 다시 획득
try {
System.out.println("methodB 진입");
System.out.println("methodB 종료");
} finally {
lock.unlock();
}
}
}
- 어떤 스레드가 methodA를 호출하여 ReentrantLock을 획득한다.
- 해당 스레드가 methodA 내부에서 methodB를 호출한다.
- methodB는 ReentrancyExample의 동일한 lock을 요구한다.
- 해당 스레드는 methodA에서 걸어놓은 본인의 lock 때문에, methodB의 진입이 막힌다?
하지만 이러한 문제는 발생하지 않는다. 왜냐하면 synchronized, ReentrantLock은 모두 Lock Reentrancy(재진입성)을 보장하기 때문이다. Lock Reentrancy란, 이미 lock을 가진 스레드가 동일한 lock을 다시 요청해도 허용되는 성질이다.
여기서 말하는 Reentrancy는 함수 관점의 Reentrancy와는 다르다. 때문에 Lock Reentrancy라고 구별해서 서술하였다.