Java Adv 08 - ReentrantLock
in Programming Language on Java
LockSupport의 한계
락(lock)이라는 클래스를 만드는 것은 특정 스레드가 먼저 락을 얻으면 RUNNABLE 상태로 실행하고, 락을 얻지 못하면 park()를 사용하여 대기 상태로 만드는 것이다. 스레드가 임계 영역의 실행을 마치고 나면 락을 반납하고, unpark()를 사용하여 대기 중인 다른 스레드를 깨우는 방식으로 작동한다. 또한, parkNanos()를 사용하여 너무 오래 대기하면 스레드가 스스로 중간에 깨어나게 할 수 있다. 하지만 이러한 기능을 직접 구현하기는 매우 어렵다. 예를 들어, 스레드 10개를 동시에 실행했을 때, 그중에 단 1개의 스레드만 락을 가질 수 있도록 락 기능을 만들어야 하며, 나머지 9개의 스레드는 대기해야 한다. 이때 어떤 스레드가 대기하고 있는지를 알 수 있는 자료구조가 필요하다. 그래야 대기 중인 스레드를 찾아서 깨울 수 있다. 여기서 끝나지 않는다. 대기 중인 스레드 중 어떤 스레드를 깨울지에 대한 우선순위를 결정하는 것도 필요하다. 결론적으로, LockSupport는 너무 저수준의 기능이다. synchronized처럼 더 고수준의 기능이 필요하다. 하지만 걱정하지 말자. 자바는 Lock 인터페이스와 ReentrantLock이라는 구현체로 이러한 기능들을 이미 다 구현해 두었다. ReentrantLock은 LockSupport를 활용하여 synchronized의 단점을 극복하면서도 매우 편리하게 임계 영역을 다룰 수 있는 다양한 기능을 제공한다.
ReentrantLock
자바는 1.0부터 존재한 synchronized 와 BLOCKED 상태를 통한 통한 임계 영역 관리의 한계를 극복하기 위해 자바 1.5부터 Lock 인터페이스와 ReentrantLock 구현체를 제공한다.
synchronized 단점
- 무한 대기:
BLOCKED상태의 스레드는 락이 풀릴 때 까지 무한 대기한다. - 특정 시간까지만 대기하는 타임아웃X
- 중간에 인터럽트X
- 공정성: 락이 돌아왔을 때
BLOCKED상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없다. - 최악의 경우 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있다.
Lock 인터페이스는 synchronized 블록보다 더 많은 유연성을 제공한다. 특히 락을 특정 시간 만큼만 시도하거나, 인터럽트 가능한 락을 사용할 때 유용하다. 다양한 메서드를 통해 synchronized의 단점인 무한 대기 문제도 깔끔하게 해결할 수 있다. 참고 lock() 메서드는 인터럽트에 응하지 않는다고 되어 있다. 이 메서드의 의도는 인터럽트가 발생해도 무시하고 락을 기다리도록 하는 것이다. 대기(WAITING) 상태의 스레드에 인터럽트가 발생하면 대기 상태를 빠져나온다고 알려져 있다. 그러나 lock() 메서드의 설명에 따르면 대기(WAITING) 상태인데 인터럽트에 응하지 않는다고 되어 있어 혼란스러울 수 있다. lock()을 호출하여 락을 얻기 위해 대기 중인 스레드에 인터럽트가 발생하면 순간적으로 대기 상태를 빠져나오는 것은 맞다. 이때 스레드는 아주 짧게 WAITING 상태에서 RUNNABLE 상태로 변경된다. 그러나 lock() 메서드 안에서 해당 스레드를 다시 WAITING 상태로 강제로 변경해버리기 때문에, 결국 인터럽트를 무시하게 되는 것이다. 인터럽트가 필요한 경우에는 lockInterruptibly() 메서드를 사용하면 된다. 새로운 Lock은 개발자에게 다양한 선택권을 제공한다.
주어진 0.5 초의 시간 동안 락 획득을 시도한다. 주어진 시간 안에 락을 획득하면 true 를 반환한다. 주어진 시간이 지나도 락을 획득하지 못한 경우 false 를 반환한다. 이 메서드는 대기 중 인터럽트가 발생하면 InterruptedException 이 발생하며 락 획득을 포기한다. ex) 맛집에 줄을 서지만 특정 시간 만큼만 기다린다. 특정 시간이 지나도 계속 줄을 서야 한다면 포기한다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기한다.
Reference
김영한님의 자바 강의