자바에서 기존 synchronized 단점을 극복하기 위해 도입하였다.
동기화에는 무한 대기, 나중에 들어온 스레드가 먼저 락을 획득하는 공정성 문제가 있었는데 ReentrantLock을 사용하면 락을 더 유연하게 사용할 수 있게 된다.
정확히는 Lock 인터페이스를 구현한 구현체다. 인터페이스에는 락을 조절할 수 있는 다향한 메서드가 있다.
참고로 여기서 쓰이는 락은 모니터 락이 아니다. Lock 인터페이스의 락이 쓰인다.
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // ReentrantLock 인스턴스 생성
public void increment() {
lock.lock(); // 락 획득
try {
// 이 블록 내의 코드는 한 번에 하나의 스레드만 실행할 수 있습니다.
count++;
System.out.println(Thread.currentThread().getName() + ": Count = " + count);
} finally {
lock.unlock(); // 락 해제 (매우 중요! finally 블록에서 호출하여 예외 발생 시에도 락이 풀리도록 함)
}
}
public int getCount() {
// 읽기 작업이더라도 일관성 유지를 위해 락을 사용할 수 있습니다.
// 여기서는 간단한 예시이므로 락을 사용하지 않았지만,
// 복잡한 읽기 작업이나 쓰기 작업과 섞이는 경우에는 고려할 수 있습니다.
return count;
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
// 5개의 스레드 생성
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment(); // 각 스레드가 1000번씩 카운터 증가
}
}, "Thread-" + (i + 1));
}
// 스레드 시작
for (Thread thread : threads) {
thread.start();
}
// 모든 스레드가 종료될 때까지 대기
for (Thread thread : threads) {
thread.join();
}
System.out.println("--- 최종 결과 ---");
System.out.println("최종 카운트: " + example.getCount()); // 기대값: 5 * 1000 = 5000
}
}
예제에서 볼 수 있듯이 private final ReentrantLock lock = new ReentrantLock(); 형식으로 만들 수 있다.
메서드는 간단하게 락이름.메서드()로 사용하면 된다. 예제에서 하듯이 공유 자원이 있는 임계 영역에 걸어주면 된다. 어려울 건 없다.
사용하면 기존 동기화의 공정성 문제를 해결할 수 있다.
private final ReentrantLock lock = new ReentrantLock(true);로 하면 공정성 모드가 된다.
이렇게 되면 가장 오래 기다린 스레드가 먼저 락을 획득하게 된다. 즉 선입선출이 보장되므로 오래 기다린 스레드가 먼저 락을 획득하지 못하는 공정성 문제가 해결된다.
다만 성능 저하가 발생된다. 아무래도 순서를 신경쓰기 때문이다.
따라서 정말로 필요한 경우에만 사용하는 게 좋다.