ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java Multithreading] Deadlock 해결 방안 2 - ReentrantLock
    Java/Multi Threading 기초 2022. 12. 16. 14:07

    Java Multithreading

    멀티스레딩의 개념과 Java에서의 활용법을 공부하고 정리하는 시리즈입니다.


    안녕하세요! 오늘은 교착상태(Deadlock)을 해결하는 또 다른 솔루션에 대해 살펴보겠습니다.

    ReentrantLock Class 알아보기

    java.util.concurrent.locks로 제공되는 ReentrantLock 클래스에 대해 살펴보겠습니다. ReentrantLockLock 인터페이스의 구현체입니다. 마치 Synchronized와 같은 락을 제공하지요. 하지만 더 유용한 확장 기능을 함께 제공하고 있어요.

    Reentrant(재진입성) 키워드에 대한 이야기는 임계 영역과 동기화에 대해 설명하는 글 마지막에 부연설명으로 드렸던 적이 있기 때문에, 그 내용은 참고 부탁드리고요. 😇

    synchronized와 다르게 ReentrantLock을 이용해서 락을 얻고 해제하는 과정은 명시적으로 이뤄집니다.

    class Example {
        private Object resource = new SomeClass();
        private final ReentrantLock rLock = new ReentrantLock();
    
        public void method() {
            rLock.lock(); // 명시적인 잠금
    
            try {
                doSomethingWith(resource); // 프로그램 내용(임계영역)
            }
            finally {
                rLock.unlock(); // 명시적인 해제
            }
        }
    
    }

    위의 코드와 같은 식으로 말이죠. try ~ finally 블록이 등장한 이유는, 프로그램을 실행하다가 예외가 발생해서 중단되더라도 획득했던 락을 반드시 반납시키기 위해서예요. 예외가 발생했다고 락을 반납하지 못하면 try로 감싸진 프로그램 실행부분(즉, 임계영역)에 교착상태가 발생하겠죠.

    참고로 ReentrantLock의 생성자는 boolean 값을 받아 생성될 수도 있습니다. true를 받아 생성되게 되면, 락을 여러 스레드에 최대한 공정하게 분배하게 됩니다. 하지만 공정하게 분배한다는게 무조건 좋은 것은 아니에요. 여러 스레드의 작업이 막힘없이 공정하게 진행되어야 한다든지(마치 UI같이), 최대한의 실시간성을 보장해야 한다든지(예를 들어 트레이딩 시스템) 하는 경우처럼, 반드시 필요한 상황이 아니라면 사용하지 않는 편이 좋다고 하네요. 오히려 처리량이 떨어질 수도 있으니까요.

    ReentrantLock으로 Lock을 획득하기 위해...

    만약에 다른 스레드가 락을 소유했는데, 그걸 모르고 락을 획득하려고 하면 어떻게 될까요? 락이 해제되기까지 쭉 대기를 하겠죠. 그리고 그건, 지난 시간까지 쭉 설명했듯이, 어쩌면 데드락 발생 조건이 될 수도 있어요.

    ReentrantLock을 사용해도 그럴까요? 네, 똑같습니다. 😅 아니, 더 좋은 기능을 제공한다고 했는데 똑같이 대기를 하다니 어떻게 될 걸까요? 굳이 ReentrantLock을 사용할 이유가 있을까요? 그것 또한 사실입니다. 🤗

    ReentrantLock은 락킹 상태를 알아보기 위한 유용한 쿼리 메서드를 제공하고 있어요. 몇 가지를 소개해 보겠습니다.

    1. isLocked() : 락이 걸려있는지 아닌지 알려줍니다.
    2. getOwner() : 현재 락을 갖고 있는 스레드를 리턴합니다.
    3. getQueuedThreads() : 락을 얻기 위해 대기하고 있는 스레드큐를 Collection으로 반환합니다.

    이외에도, 해당 자원의 락킹 상태에 대한 여러 정보를 얻을 수 있는 유용한 메서드들이 제공되고 있으니 스펙을 참고해보시는 것도 좋을 것 같아요.

    쿼리메소드가 제공된다고 해도...

    이쯤에서 질문이 하나 떠오르실 수도 있는데요. '이상하다, 쿼리메소드가 제공된다고 해도 상태를 확인한 뒤 락을 얻는 과정에서 데드락이 발생할 수 있는 건 마찬가지 아닌가?'라는 생각이 드셨다면 정말 잘 따라오고 계신 거예요!

    이 문제를 해결하기 위해 ReentrantLock은 몇 가지 솔루션을 제공합니다.

    먼저 ReentrantLocktryLock()이라는 아주 유용한 메소드를 제공하고 있어요. 이름 그대로, 락을 얻기 위해 시도하는 것이죠.
    tryLock()은 다른 스레드에 의해 잠겨있지 않은 상태에서만 락을 반환하고, 이를 boolean 값으로 리턴합니다.

    그리고 lockInterruptibly()라는 메소드 또한 제공됩니다. lock() 메소드로 얻은 락은 다른 동기화 방식과 마찬가지로, 한 스레드가 락을 획득하고 나면 다시 반납하기 전까지 다른 스레드들은 유휴 상태가 됩니다. 문제는, 이 유휴 상태에서 깨울 방법이 없다는 것이죠. 중단된 스레드를 깨우는 메소드인 Thread#interrupt()를 호출하더라도 이 경우엔 소용이 없습니다. 살아있는 스레드가 존재하니, 프로그램을 정상 종료할 수도 없습니다.
    하지만 lockInterruptibly()로 동기화를 시도할 경우, 대기하며 유휴 상태에 들어가더라도 인터럽트가 가능합니다. 그리고 아래 코드와 같이 인터럽트 되는 상황에 따른 적절한 종료 처리를 할 수 있겠죠.

    @Override 
    public void run() {
      while (true) {
          try {
            lockObject.lockInterruptibly();
            // ... do something
          }
          catch (InterruptedException e) {
            cleanupAndExit();
          }
      }
    }

    결론

    이번 글을 통해서 ReentrantLock을 통해서 좀 더 안전한 동기화가 가능해진 것을 확인했습니다.

    • 여러 쿼리 메소드가 제공되었고,
    • lockInterruptibly()를 통해서 인터럽트 가능한 프로그램을 구현할 수 있었고,
    • tryLock()을 통해 완전히 안전한 락킹이 가능해졌습니다.

    다음 포스팅에는... 무엇을 알아볼지 아직 못 정했는데요! 😅
    그래도 벌써 Java를 이용한 멀티스레딩 프로그래밍에 대해 꽤 많이 알아보았는데요! 🤗 여기까지 오기가 참 재밌고 어려웠던 것 같습니다. ㅎㅎ
    아마도 다음 주제쯤이 마지막이 아닐까 싶은데요. 잘 마무리 할 수 있는 주제들을 가지고 다음 멀티스레딩 포스팅으로 돌아오겠습니다.

    감사합니다! 👍

    댓글

Designed by Tistory.