Home 자바 동시성 문제 해결하기 -2 (ReentrantLock)
Post
Cancel

자바 동시성 문제 해결하기 -2 (ReentrantLock)

이전 게시글과 이어집니다.

이번에는 저번 게시글에서 소개한 synchronized 의 한계를 보완한 ReentrantLock 을 사용하여 동시성 문제를 해결해 보겠습니다.

ReentrantLock

우선 ReentrantLock 에 대해 알아보겠습니다. ReentrantLock 은 자바 1.5 버전부터 추가된 클래스이며, synchronized 보다 더 많은 제어권을 제공합니다.

특징

  • 락을 명시적으로 획득/해제할 수 있습니다. synchronized 키워드와 달리 코드로 직접 락을 획득하고 반납하기 때문에 명시적입니다.

  • 인터럽트 대응이 가능합니다. 락을 획득하기 위해 대기 중 인터럽트가 발생하면 락 획득을 포기합니다.

  • tryLock() 으로 락 획득 실패 로직 작성이 가능합니다. 락 획득을 시도하고 실패한다면 조건문을 사용해 실패 로직을 작성할 수 있습니다.

  • 공정성(Fairness) 설정이 가능합니다. 락 객체 생성 시 옵션을 주어 락을 요청한 순서대로 락을 획득하도록 설정이 가능합니다.

추가적으로, ReentrantLock 이 사용하는 락은 객체 내부에 있는 모니터 락이 아닙니다. 모니터락은 synchronized 에서만 사용됩니다.

예제 코드

전체 코드

저번 게시글에서 synchronized 를 통해 동시성을 해결했던 코드를 발전시켜 ReentrantLock 을 사용한 코드로 변경해 보겠습니다.

ReentrantLock 을 사용한 문제 해결 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * 계좌 클래스 (ReentrantLock 사용)
 */
public class AccountV3 {

    private int balance;
    private ReentrantLock lock = new ReentrantLock();

    public AccountV3() {
        this.balance = 0;
    }

    /**
     * 잔액 조회 메서드
     * @return 처리 결과
     */
    public int getBalance() {
        String name = Thread.currentThread().getName();
        lock.lock();
        try {
            System.out.println("[" + name + "] 잔액을 조회합니다. 잔액: " + this.balance);
            return balance;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 입금 메서드
     * @param amount 입금할 금액
     * @return 처리 결과
     */
    public boolean deposit(int amount) {
        String name = Thread.currentThread().getName();
        lock.lock();
        try {
            System.out.println("[" + name + "] 입금을 시도합니다. 입금 전 잔액: " + this.balance);
            Thread.sleep(1000L);
            this.balance += amount;
            System.out.println("[" + name + "] 입금이 완료되었습니다. 입금 후 잔액: " + this.balance);
            return true;
        } catch (InterruptedException e) {
            System.out.println("[" + name + "] 입금에 실패했습니다");
            return false;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 출금 메서드
     * @param amount 출금할 금액
     * @return 처리 결과
     */
    public boolean withdraw(int amount) {
        String name = Thread.currentThread().getName();
        lock.lock();
        try {
            System.out.println("[" + name + "] 출금을 시도합니다. 출금 전 잔액: " + this.balance);
            Thread.sleep(2000L);
            if (this.balance < amount) {
                System.out.println("[" + name + "] 잔액보다 많은 양을 출금할 수 없습니다.");
                return false;
            }
            this.balance -= amount;
            System.out.println("[" + name + "] 출금이 완료되었습니다. 출금 후 잔액: " + this.balance);
            return true;
        } catch (InterruptedException e) {
            System.out.println("[" + name + "] 출금에 실패했습니다");
            return false;
        } finally {
            lock.unlock();
        }
    }
}

finally 블록에서 unlock()반드시 호출해야 락 해제를 보장할 수 있습니다. 호출 누락 시 락을 반납하지 않아 무한 대기에 빠질 수 있습니다.

lock(), unlock() 메서드를 사용하여 임계 영역에 들어가기 전에 락을 획득하고, 임계 영역이 끝나면 락을 반환합니다.

이 코드를 멀티 스레드 환경에서 실행해 보겠습니다.

락을 이용하여 각 메서드를 한 번에 한 스레드에서만 실행되게 하여 동시성 문제를 해결하였습니다.

하지만, 이 코드 역시 synchronized 와 똑같이 무한 대기 문제가 발생할 가능성이 존재합니다.

ReentrantLock 을 사용한 문제 해결 2

이번에는 락 획득을 시도하고 실패하면 이전과 같이 대기하는 것이 아니라 바로 포기하고 다음 로직으로 넘어가도록 작성해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * 계좌 클래스 (ReentrantLock 로 무한 대기 해결)
 */
public class AccountV4 {

    private int balance;
    private ReentrantLock lock = new ReentrantLock();

    public AccountV4() {
        this.balance = 0;
    }

    /**
     * 잔액 조회 메서드
     * @return 처리 결과
     */
    public int getBalance() {
        String name = Thread.currentThread().getName();
        lock.lock();
        try {
            System.out.println("[" + name + "] 잔액을 조회합니다. 잔액: " + this.balance);
            return balance;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 입금 메서드
     * @param amount 입금할 금액
     * @return 처리 결과
     */
    public boolean deposit(int amount) {
        String name = Thread.currentThread().getName();
        if (!lock.tryLock()) { // 락 확보를 시도하고, 실패하면 반환한다.
            System.out.println("[" + name + "] **실패** 이미 다른 스레드에서 작업입니다... ");
            return false;
        }
        try {
            System.out.println("[" + name + "] 입금을 시도합니다. 입금 전 잔액: " + this.balance);
            Thread.sleep(1000L);
            this.balance += amount;
            System.out.println("[" + name + "] 입금이 완료되었습니다. 입금 후 잔액: " + this.balance);
            return true;
        } catch (InterruptedException e) {
            System.out.println("[" + name + "] 입금에 실패했습니다");
            return false;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 출금 메서드
     * @param amount 출금할 금액
     * @return 처리 결과
     */
    public boolean withdraw(int amount) {
        String name = Thread.currentThread().getName();
        if (!lock.tryLock()) { // 락 확보를 시도하고, 실패하면 반환한다.
            System.out.println("[" + name + "] **실패** 이미 다른 스레드에서 작업입니다... ");
            return false;
        }
        try {
            System.out.println("[" + name + "] 출금을 시도합니다. 출금 전 잔액: " + this.balance);
            Thread.sleep(2000L);
            if (this.balance < amount) {
                System.out.println("[" + name + "] 잔액보다 많은 양을 출금할 수 없습니다.");
                return false;
            }
            this.balance -= amount;
            System.out.println("[" + name + "] 출금이 완료되었습니다. 출금 후 잔액: " + this.balance);
            return true;
        } catch (InterruptedException e) {
            System.out.println("[" + name + "] 출금에 실패했습니다");
            return false;
        } finally {
            lock.unlock();
        }
    }
}

이 코드를 멀티 스레드 환경에서 실행해 보겠습니다.

락을 획득한 스레드에서만 입금을 시도하고, 락을 획득하지 못한 스레드는 바로 실패하여 다음 로직을 수행합니다.

정리

이처럼 ReentrantLocksynchronized 보다 유연한 락 제어를 가능하게 하며, 동시성 제어가 필요한 복잡한 시스템에서 매우 유용합니다. 하지만 단순한 경우에는 오히려 복잡성만 증가할 수 있으므로 synchronized 가 더 간결하고 좋은 선택일 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

(Troubleshooting) 자바 리플렉션 사용 시 주의 사항

-