ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java ReentrantLock 알아보기
    Java 2024. 6. 3. 06:08
    728x90
    반응형

     

    - 목차

     

    들어가며.

    JavaReentrantLock 은 멀티스레드 환경에서 공유 자원에 대한 접근을 제어하기 위한 동기화 도구입니다.

    ReentrantLockjava.util.concurrent 패키지에 포함된 동시성 제어 클래스 중 하나로

    기본적으로 synchronized 키워드와 유사한 역할을 하지만, 더 많은 기능과 유연성을 제공합니다.

     

    일반적으로 동기화를 구현하기 위해 MutexSemaphore 와 같은 메커니즘이 사용됩니다.

    ReentrantLockMutex 에 해당하는 Java 의 동기화 도구입니다.

     

    MutexMutual Exclusion 의 약자로, 하나의 자원(Resource) 에 대해 동시에 여러 스레드가 접근하지 못하도록 보장하는 동기화 메커니즘입니다.

    즉, 한 시점에 오직 하나의 Thread 만이 접근할 수 있습니다.

    이를 통해 공유 자원의 데이터 무결성(Data Integrity) 을 유지할 수 있습니다.

     

    ReentrantLock 사용해보기.

     

    먼저 Mutex Lock 을 사용하지 않고 여러 쓰레드가 공유 자원을 수정하는 예시를 보여드리려고 합니다.

    전통적인 예시로 Bank 객체의 sharedResource 를 다중 쓰레드들이 업데이트하는 상황입니다.

     

    총 100개의 Thread 들이 shareResource 를 증감시키는 동작을 취하며, 동기화가 되지 않는 상황에서 결과값을 예측할 수 없습니다.

    class BankWithOutLock {
        private int shareResource;
    
        BankWithOutLock() {
            this.shareResource = 1;
        }
    
    
        void increment() {
            System.out.println(Thread.currentThread().getName() + " is trying to increment shareResource");
            this.shareResource += 1;
            System.out.println(Thread.currentThread().getName() + " succeeds to increment shareResource " + this.shareResource);
        }
    
        void decrement() {
            System.out.println(Thread.currentThread().getName() + " is trying to decrement shareResource");
            this.shareResource -= 1;
            System.out.println(Thread.currentThread().getName() + " succeeds to decrement shareResource " + this.shareResource);
        }
    
        public static void main (String[] args) {
            BankWithOutLock bank = new BankWithOutLock();
            ExecutorService executorService = Executors.newFixedThreadPool(100);
    
            for (int i = 0; i < 50; i++) {
                executorService.submit(bank::increment);
                executorService.submit(bank::decrement);
            }
            executorService.shutdown();
    
        }
    }
    pool-1-thread-69 succeeds to increment shareResource -4
    pool-1-thread-76 is trying to decrement shareResource
    pool-1-thread-76 succeeds to decrement shareResource 2
    // ... 
    pool-1-thread-63 succeeds to increment shareResource -6
    pool-1-thread-47 succeeds to increment shareResource -3
    pool-1-thread-25 succeeds to increment shareResource -8
    pool-1-thread-86 succeeds to decrement shareResource -5
    pool-1-thread-75 succeeds to increment shareResource -5

     

     

    ReentrantLock 의 lock , unlock 함수.

    이제 lock 과 unlock 함수를 통해서 동기화를 적용해보도록 하겠습니다.

     

    아래의 스크립트 내용은 50개의 Thread 가 shareResource 를 Increment 시도를 하고,

    나머지 50개의 Thread 들이 shareResource 에 Decrement 를 시도합니다.

    increment 와 decrement 함수의 내용은 lock() 함수의 호출로 시작하여, unlock() 함수의 호출로 종료됩니다.

    lock() 과 unlock() 사이의 실행 내용을 임계영역으로 만들어, 동기화가 적용됩니다.

     

    class Bank {
        private final ReentrantLock lock = new ReentrantLock();
        private int shareResource;
    
        Bank() {
            this.shareResource = 1;
        }
    
    
        void increment() {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " is trying to increment shareResource");
            this.shareResource += 1;
            System.out.println(Thread.currentThread().getName() + " succeeds to increment shareResource " + this.shareResource);
            lock.unlock();
        }
    
        void decrement() {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " is trying to decrement shareResource");
            this.shareResource -= 1;
            System.out.println(Thread.currentThread().getName() + " succeeds to decrement shareResource " + this.shareResource);
            lock.unlock();
        }
    
        public static void main (String[] args) {
            Bank bank = new Bank();
            ExecutorService executorService = Executors.newFixedThreadPool(100);
    
            for (int i = 0; i < 50; i++) {
                executorService.submit(bank::increment);
                executorService.submit(bank::decrement);
            }
            executorService.shutdown();
    
        }
    }
    pool-1-thread-1 is trying to increment shareResource
    pool-1-thread-1 succeeds to increment shareResource 2
    pool-1-thread-2 is trying to decrement shareResource
    pool-1-thread-2 succeeds to decrement shareResource 1
    pool-1-thread-5 is trying to increment shareResource
    pool-1-thread-5 succeeds to increment shareResource 2
    // ...
    pool-1-thread-98 is trying to decrement shareResource
    pool-1-thread-98 succeeds to decrement shareResource 1
    pool-1-thread-99 is trying to increment shareResource
    pool-1-thread-99 succeeds to increment shareResource 2
    pool-1-thread-100 is trying to decrement shareResource
    pool-1-thread-100 succeeds to decrement shareResource 1

     

    사실상 이러한 lock() 과 unlock() 함수를 통한 임계영역은 쓰임새가 단순하여 synchronized 키워드와 동일한 역할을 수행합니다.

     

    ReentrantLock 의 tryLock 함수.

    ReentrantLock 은 tryLock 이라는 함수를 제공합니다.

    이는 정해진 시간 동안 Lock 의 획득을 시도합니다. 그리고 그 시간동안 Lock 을 획득하거나 실패하게 되는데요.

    획득에 성공한 경우에는 true 를 반환하게 되고, 실패한 경우에는 false 를 반환합니다.

     

    tryLock 의 유즈케이스는 실행 시간을 예측하기 힘든 경우에 사용되는데요.

    예를 들어, 한 Thread 가 File/Network IO 의 통신 시간이 길어져서 다른 Thread 가 Lock 을 획득하기에 시간이 오래 걸린다면,

    차라리 이번 시도를 취소하고 Retry 를 시도하는 편이 더 효율적일 수 있습니다.

     

    예를 들어보겠습니다.

    아래의 예시는 increment 함수와 decrementLongRunning 함수가 존재합니다.

    increment 함수는 shareResource 를 1씩 증가시키는 역할을 수행합니다.

    decrementLongRunning 함수는 shareResource 는 1씩 감소시키는 역할을 수행합니다.

    decrementLongRunning 함수는 1초의 Blocking 시간을 부여하였으며, 최소 1초 동안 Lock 을 소유하고 release 하지 않습니다.

    따라서 increment 함수는 tryLock 함수를 호출할 때에 1초 동안 Lock 을 획득하지 못한다면 retry 를 재귀적으로 시도합니다.

     

    class Bank {
        private final ReentrantLock lock = new ReentrantLock();
        private int shareResource;
    
        Bank() {
            this.shareResource = 1;
        }
    
    
        void increment() {
    
            boolean acquired = false;
            // 1초 동안 Lock 을 획득하기 위해 시도한다.
            try {
                acquired = lock.tryLock(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {}
            
            // 1초 안에 Lock 을 획득한 경우.
            if (acquired) {
                System.out.println(Thread.currentThread().getName() + " is trying to increment shareResource");
                this.shareResource += 1;
                System.out.println(Thread.currentThread().getName() + " succeeds to increment shareResource " + this.shareResource);
                lock.unlock();
            } else {
                // 1초 안에 Lock 획득을 실패한 경우.
                System.out.println(Thread.currentThread().getName() + " is failed to get lock and retry");
                // 1초 간 대기 이후에 retry 시도. 
                try {Thread.sleep(1000);} catch (InterruptedException e) {}
                increment();
            }
        }
    
        void decrementLongRunning() {
    
            // Lock 을 획득하기 위해 대기. 
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " is trying to decrement shareResource");
            this.shareResource -= 1;
            try {Thread.sleep(1000L);} catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " succeeds to decrement shareResource " + this.shareResource);
            lock.unlock();
        }
    
        public static void main (String[] args) {
            Bank bank = new Bank();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
    
            for (int i = 0; i < 10; i++) {
                executorService.submit(bank::increment);
                executorService.submit(bank::decrementLongRunning);
            }
            executorService.shutdown();
    
        }
    }
    pool-1-thread-1 is trying to increment shareResource
    pool-1-thread-1 succeeds to increment shareResource 2
    pool-1-thread-2 is trying to decrement shareResource
    pool-1-thread-3 is failed to get lock and retry
    pool-1-thread-13 is failed to get lock and retry
    pool-1-thread-5 is failed to get lock and retry
    pool-1-thread-7 is failed to get lock and retry
    pool-1-thread-11 is failed to get lock and retry
    // ...
    pool-1-thread-19 is trying to increment shareResource
    pool-1-thread-19 succeeds to increment shareResource -2
    pool-1-thread-3 is trying to increment shareResource
    pool-1-thread-3 succeeds to increment shareResource -1
    pool-1-thread-11 is trying to increment shareResource
    pool-1-thread-11 succeeds to increment shareResource 0
    pool-1-thread-13 is trying to increment shareResource
    pool-1-thread-13 succeeds to increment shareResource 1

     

     

    반응형
Designed by Tistory.