Java

[Thread] Lock, ReetrantLock

caporatang 2024. 9. 7. 18:22
반응형

ReetrantLock

자바 1.0부터 지원하기 시작한 synchronized와 BLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해 자바 1.5부터 Lock 인터페이스, ReentrantLock 구현체를 제공한다.

package java.util.concurrent.locks;

import java.util.concurrent.TimeUnit;

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

Lock 인터페이스는 안전한 임계 영역을 위한 락을 구현하는데 사용한다.

  • void lock()
    • 락을 획득한다. 만약 다른 스레드가 이미 락을 획득했다면, 락이 풀릴 때까지 현재 스레드는 대기한다. 이 메서드는 인터럽트에 응답하지 않는다.
      • 여기서 뜻하는 락은 객체 내부에 있는 모니터 락이 아니다. Lock 인터페이스와 ReentrantLock 이 제공하는 기능이다. 모니터 락과 BLOCKED 상태는 synchronized 에서만 사용된다.
  • void lockInterruptibly()
    • 락 획득을 시도하지만, 다른 스레드가 인터럽트할 수 있도록 한다. 만약 다른 스레드가 이미 락을 획득했다면, 현재 스레드는 락을 획득할 때까지 대기하고, 대기 중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득을 포기한다.
  • boolean tryLock()
    • 락 획득을 시도하고, 즉시 성공 여부를 반환한다. 만약 다른 스레드가 이미 락을 획득했다면 false, 그렇지 않으면 true를 반환한다.
  • boolean tryLock(long time, TimeUnit unit)
    • 주어진 시간 동안 락 획득을 시도, 시간 안에 락을 획득하면 true를 반환한다. 획득하지 못하면 false를 반환한다. 대기 중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득을 포기한다.
  • void unlock()
    • 락을 해제한다. 락을 해제하면 락 획득을 대기 중인 스레드 중 하나가 락을 획득할 수 있게 된다. 락을 획득한 스레드가 호출해야 하며, 그렇지 않으면 IllegalMonitorStateException이 발생한다.
  • Condition newCondition()
    • Condition 객체를 생성하여 반환한다. Condition 객체는 락과 결합되어 사용되며, 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 한다.

공정성

Lock 인터페이스의 대표적인 구현체로 ReetrantLock을 사용하면 공정하게 스레드를 획득할 수 있게끔 설정할 수 있다.

import java.util.concurrent.locks.ReentrantLock;

public class ReetrantLockTest {
    // 비공정 모드 락
    private final Lock nonFairLock = new ReentrantLock();

    // 공정 모드 락 
    private final Lock fairLock = new ReetrantLock(true);

    public void nonFairLockTest() {
        nonFairLock.lock();
        try {
            /* 임계영역 do something .. */
        } finally {
          nonFairLock.unlock();
        }
    }

  public void nonFairLockTest() {
    fairLock.lock();
    try {
      /* 임계영역 do something .. */
    } finally {
      fairLock.unlock();
    }
  }
}

비공정 모드는 락을 획득하는 속도가 빠르고, 새로운 스레드가 기존 대기 스레드보다 먼저 락을 획득할 수 있다. 기아 현상이 발생할 수 있는데, 기아 현상은 특정 스레드가 계속해서 락을 획득하지 못할 수 있는 현상을 뜻한다.
반대로 공정 모드는 락을 요청한 순서대로 스레드가 락을 획득할 수 있게 한다. 대기 큐에서 먼저 대기한 스레드가 락을 먼저 획득하고, 기아 현상이 방지 된다는 특징이 있지만 성능이 떨어질 수 있다.

ReentrantLock의 활용

public class BankAccountV4 implements BankAccount {

    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV4(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public  boolean withdraw(int amount) {
        log("거래 시작 " + getClass().getSimpleName());

        lock.lock(); // ReentrantLock을 이용하여 lock

        try {
            log("[검증 시작] 출금액 : " + amount + ", 잔액 : " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액 : " + amount + ", 잔액 : " + balance);
                return false;
            }
            log("[검증 완료] 출금액 : " + amount + ", 잔액 : " + balance);

            sleep(1000);
            balance = balance - amount;

            log("[출금 완료] 출금액 : " + amount + ", 잔액 : " + balance);
        } finally {
            // lock을 사용했으면 반드시 unlock
            // 에러가 터지거나 다른 문제가 생겼을 때도 반드시 unlock이 되어야 하므로 try - finally 사용
            lock.unlock();
        }
        log("거래 종료 ");

        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
}
17:58:45.523 [       t1] 거래 시작 BankAccountV4
17:58:45.523 [       t2] 거래 시작 BankAccountV4
17:58:45.549 [       t1] [검증 시작] 출금액 : 800, 잔액 : 1000
17:58:45.550 [       t1] [검증 완료] 출금액 : 800, 잔액 : 1000
17:58:45.821 [     main] thread1 state : TIMED_WAITING
17:58:45.821 [     main] thread2 state : WAITING
17:58:46.557 [       t1] [출금 완료] 출금액 : 800, 잔액 : 200
17:58:46.558 [       t1] 거래 종료 
17:58:46.558 [       t2] [검증 시작] 출금액 : 800, 잔액 : 200
17:58:46.562 [       t2] [검증 실패] 출금액 : 800, 잔액 : 200
17:58:46.574 [     main] 최종 잔액 : 200

synchronized 보다 더 유연하게 사용할 수 있다. 다만, synchronized는 대기하는 스레드의 상태가 BLOCKED였고 ReentrantLock을 사용하면 대기하는 스레드의 상태는 WAITING이다.

대기 중단

tryLock(), tryLock(long time, TimeUnit unit) 을 사용하면 대기를 중간에 중단시킬 수 있다.

public class BankAccountV5 implements BankAccount {

    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV5(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public  boolean withdraw(int amount) {
        log("거래 시작 " + getClass().getSimpleName());

        if (!lock.tryLock()) {
            // 획득할 lock이 없는 경우 바로 return
            log("[진입 실패] 이미 처리중인 작업 존재");
            return false;
        }

      // 시간 지정
      //try {
      //  if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
      //    // 0.5초간 대기하다가 락을 획득하지 못하면 false를 반환하면서 메서드를 빠져나옴
      //    // 스레드의 상태는 대기하는 동안 TIMED_WAITING, 대기 상태를 빠져나오면 RUNNABLE
      //    log("[진입 실패] 이미 처리중인 작업 존재");
      //    return false;
      //  }
      //} catch (InterruptedException e) {
      //  throw new RuntimeException(e);
      //}

        try {
            log("[검증 시작] 출금액 : " + amount + ", 잔액 : " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액 : " + amount + ", 잔액 : " + balance);
                return false;
            }
            log("[검증 완료] 출금액 : " + amount + ", 잔액 : " + balance);

            sleep(1000);
            balance = balance - amount;

            log("[출금 완료] 출금액 : " + amount + ", 잔액 : " + balance);
        } finally {
            lock.unlock();
        }
        log("거래 종료 ");

        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
}
시간 지정하지 않은 결과
18:07:29.562 [       t1] 거래 시작 BankAccountV5
18:07:29.562 [       t2] 거래 시작 BankAccountV5
18:07:29.569 [       t2] [진입 실패] 이미 처리중인 작업 존재
18:07:29.587 [       t1] [검증 시작] 출금액 : 800, 잔액 : 1000
18:07:29.588 [       t1] [검증 완료] 출금액 : 800, 잔액 : 1000
18:07:29.878 [     main] thread1 state : TIMED_WAITING
18:07:29.880 [     main] thread2 state : TERMINATED
18:07:30.597 [       t1] [출금 완료] 출금액 : 800, 잔액 : 200
18:07:30.599 [       t1] 거래 종료 
18:07:30.618 [     main] 최종 잔액 : 200
시간 지정해준 실행 결과
18:12:37.048 [       t1] 거래 시작 BankAccountV6
18:12:37.048 [       t2] 거래 시작 BankAccountV6
18:12:37.073 [       t1] [검증 시작] 출금액 : 800, 잔액 : 1000
18:12:37.074 [       t1] [검증 완료] 출금액 : 800, 잔액 : 1000
18:12:37.382 [     main] thread1 state : TIMED_WAITING
18:12:37.383 [     main] thread2 state : TIMED_WAITING
18:12:37.558 [       t2] [진입 실패] 이미 처리중인 작업 존재
18:12:38.082 [       t1] [출금 완료] 출금액 : 800, 잔액 : 200
18:12:38.087 [       t1] 거래 종료 
18:12:38.105 [     main] 최종 잔액 : 200
반응형

'Java' 카테고리의 다른 글

[Thread] Spinlcok  (0) 2024.09.08
[Thread] LockSupport  (0) 2024.09.06
[Thread] CAS 연산  (0) 2024.09.03
[Thread] Atomic, volatile, synchronized 성능 비교  (0) 2024.09.03
[Thread] synchronized  (0) 2024.09.02