-
Java ReentrantLock 알아보기Java 2024. 6. 3. 06:08728x90반응형
- 목차
들어가며.
Java 의 ReentrantLock 은 멀티스레드 환경에서 공유 자원에 대한 접근을 제어하기 위한 동기화 도구입니다.
ReentrantLock 은 java.util.concurrent 패키지에 포함된 동시성 제어 클래스 중 하나로
기본적으로 synchronized 키워드와 유사한 역할을 하지만, 더 많은 기능과 유연성을 제공합니다.
일반적으로 동기화를 구현하기 위해 Mutex 와 Semaphore 와 같은 메커니즘이 사용됩니다.
ReentrantLock 은 Mutex 에 해당하는 Java 의 동기화 도구입니다.
Mutex 는 Mutual 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
반응형'Java' 카테고리의 다른 글
[Java] 익명 클래스와 메모리 관계 알아보기 ( Anonymous Class , Method Area ) (0) 2024.06.02 Jackson 으로 JSON 다루기 (0) 2024.01.22 Java off-heap 메모리 알아보기 (0) 2023.12.22 Java Thread Pool 알아보기 (0) 2023.09.30 Java Future 알아보기 (0) 2023.09.30