---
description: Synchronized && ReentrantLock
---

# Synchronized && ReentrantLock

## 1. 출금 예제

```java
package thread.sync;

public interface BankAccount {
    boolean withdraw(int amount);
    int getBalance();
}
```

```java
package thread.sync;

public class WithdrawTask implements Runnable {
    private BankAccount account;
    private int amount;

    public WithdrawTask(BankAccount account, int amount) {
        this.account = account;
        this.amount = amount;
    }

    @Override
    public void run() {
        account.withdraw(amount);
    }
}

```

```java
package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankMain {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccountV1(1000);

        Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
        Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
        t1.start();
        t2.start();

        sleep(500);
        log("t1 state: " + t1.getState());
        log("t2 state: " + t2.getState());

        t1.join();
        t2.join();
        log("최종 잔액: " + account.getBalance());
    }
}

```

(초기 셋업 코드)

### 1.1. 공유 자원에 대한 동시 접근 시나리오

```java
package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV1 implements BankAccount {
    private int balance;

    public BankAccountV1(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
        sleep(1000); // 출금에 걸리는 시간으로 가정
        balance = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
```

```
// 실행 결과
12:16:55:211 [       t1] 거래 시작: BankAccountV1
12:16:55:210 [       t2] 거래 시작: BankAccountV1
12:16:55:221 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
12:16:55:221 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
12:16:55:222 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
12:16:55:222 [       t2] [검증 완료] 출금액: 800, 잔액: 1000
12:16:55:688 [     main] t1 state: TIMED_WAITING
12:16:55:688 [     main] t2 state: TIMED_WAITING
12:16:56:237 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
12:16:56:237 [       t2] [출금 완료] 출금액: 800, 변경 잔액: -600
12:16:56:238 [       t1] 거래 종료
12:16:56:238 [       t2] 거래 종료
12:16:56:242 [     main] 최종 잔액: -600
```

위 상황에서는 동시에 2개의 스레드를 통해서 출금을 하고 있음

* t1, t2 스레드가거의 동시의 실행됨

과정:

<div align="left"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F9yLVwEhCVpiUcZqWT3eD%2Fimage.png?alt=media&#x26;token=90ea403a-d241-4da7-83c3-832ccb7381bd" alt=""><figcaption></figcaption></figure></div>

t1: 잔액(1000)이 출금액(800)보다 많으므로 검증 로직 통과

<div align="left"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F1I0tPFYZdU0lB6Mc1ADq%2Fimage.png?alt=media&#x26;token=d66b5e7f-e4ba-4dce-9f70-2e3ed085e030" alt=""><figcaption></figcaption></figure></div>

t2: 잔액(1000)이 출금액(800)보다 많으므로 검증 로직 통과

<div align="left"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FJzn6LEqlRV5YhXtXNEpe%2Fimage.png?alt=media&#x26;token=40efb20d-184f-4553-b638-330f83cd66ef" alt=""><figcaption></figcaption></figure></div>

<div align="left"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FYYZCl9JxJZ7v638esl19%2Fimage.png?alt=media&#x26;token=2e2a3861-f6c3-4f09-849a-3bcfd004a6cd" alt=""><figcaption></figcaption></figure></div>

t1: balance(1000) - amount(800) = 200

<div align="left"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F05bo70YLiAvJoKn9MrOM%2Fimage.png?alt=media&#x26;token=f5205624-1ab6-4f41-9878-60490039ba54" alt=""><figcaption></figcaption></figure></div>

t2: balance(200) - amount(800) = -600



다른 경우가 발생할 수도 있다.

```
12:31:10.885 [       t2] 거래 시작: BankAccountV1
12:31:10.885 [       t1] 거래 시작: BankAccountV1
12:31:10.892 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
12:31:10.892 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
12:31:10.893 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
12:31:10.893 [       t2] [검증 완료] 출금액: 800, 잔액: 1000
12:31:11.370 [     main] t1 state: TIMED_WAITING
12:31:11.370 [     main] t2 state: TIMED_WAITING
12:31:11.898 [       t2] [출금 완료] 출금액: 800, 변경 잔액: 200
12:31:11.898 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
12:31:11.899 [       t2] 거래 종료
12:31:11.899 [       t1] 거래 종료
12:31:11.905 [     main] 최종 잔액: 200
```

이러한 경우에는 balance = balance - amount부분에서 문제가 발생.

* t1: 잔액(1000)을 읽음
* t2:잔액(1000)을 읽음
* t1: 출금(800) 후 잔액(200) 저장
* t2: 출금(800) 후 잔액(200) 저장



정리하자면 두가지 케이스로 나뉜다.

1. t1 쓰레드의 출금 후 잔액 저장 전에, t2 쓰레드의 출금 여부 확인 통과.
2. t1 쓰레드의 출금 후 잔액 저장 전에, t2 쓰레드가 잔액을 불러왔을때.

### 1.2. 임계영역과Synchronized

#### 1.2.1.  임계영역(Critical Section)

* 여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 코드 부분
* 여러 스레드가 동시에 접근해서는 안되는 공유 자원을 접근하거나 수정하는 부분 (공유 변수나 공유 객체 수정)

#### 1.2.2. Synchronized

위 상황을 피하기 위해서는 synchronized 키워드를 사용할 수 있다.

```java
package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV2 implements BankAccount {
    private int balance;

    public BankAccountV2(int balance) {
        this.balance = balance;
    }

    @Override
    public synchronized boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
        sleep(1000); // 출금에 걸리는 시간으로 가정
        balance = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
```

* 단순히 메서드 앞에 synchronized를 붙여주면 된다.
* withdraw 메서드 전체가 "보호받는 임계영역"이 된다. 한번에 하나의 스레드만 접근 가능.

```
// 실행 결과
13:58:39:633 [       t1] 거래 시작: BankAccountV2
13:58:39:643 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
13:58:39:643 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
13:58:40:108 [     main] t1 state: TIMED_WAITING
13:58:40:108 [     main] t2 state: BLOCKED
13:58:40:645 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
13:58:40:645 [       t1] 거래 종료
13:58:40:646 [       t2] 거래 시작: BankAccountV2
13:58:40:646 [       t2] [검증 시작] 출금액: 800, 잔액: 200
13:58:40:647 [       t2] [검증 실패] 출금액: 800, 잔액: 200
13:58:40:650 [     main] 최종 잔액: 200
```

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FRzAM7ntko9GDdh5qUcjO%2Fimage.png?alt=media&#x26;token=6d1944b1-b3e7-4a0b-aa7f-df3f957eb8e4" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F8EqJOscG1j3bHQBDFBGH%2Fimage.png?alt=media&#x26;token=c7c8f862-6e3d-4524-9009-aaf46eec0463" alt=""><figcaption></figcaption></figure>

* t1이 모두 수행될떄까지 t2는 메서드를 시작할 수 없게 된다.
* t1이 실행될동안 t2는 **BLOCKED** 상태이다.
* 현재는 두개의 스레드만 존재하지만, 여러 스레드가 있을때 락을 획득하는 다음 스레드는 랜덤이다.
* synchronized안에서 접근하는 변수의 메모리 가시성 문제는 volatile이 없어도 해결됨(JMM의 Happens-Before)

### 1.3. Synchronized 코드 블록

위 코드를 자세히 보면, 보호가 필요없는 임계영역이 존재한다. 메서드 전체가 아닌 특정 코드 부분만 보호되는 임계영역으로 지정할 수 있다.

```java
package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV3 implements BankAccount {
    private int balance;

    public BankAccountV3(int balance) {
        this.balance = balance;
    }

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

        synchronized(this) {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000); // 출금에 걸리는 시간으로 가정
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
```

* synchronized(this)를 사용하여 코드 영역을 지정할 수 있다.

```
// 실행 결과
14:17:46:387 [       t1] 거래 시작: BankAccountV3
14:17:46:387 [       t2] 거래 시작: BankAccountV3
14:17:46:397 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
14:17:46:397 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
14:17:46:863 [     main] t1 state: TIMED_WAITING
14:17:46:863 [     main] t2 state: BLOCKED
14:17:47:399 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
14:17:47:399 [       t1] 거래 종료
14:17:47:399 [       t2] [검증 시작] 출금액: 800, 잔액: 200
14:17:47:400 [       t2] [검증 실패] 출금액: 800, 잔액: 200
14:17:47:404 [     main] 최종 잔액: 200
```

* "거래 시작" 로그는 동시에 여러 스레드가 접근해도 문제가 없기때문에, 이 부분을 제외했다.
* 로그를 봤을때 t2의 "거래   시작" 까지는 실행되는것을 볼 수 있다.

### 1.4. Synchronized의 단점

* 무한 대기: BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기
  * 특정 시간까지만 대기하는 타임아웃 불가
  * 중간에 인터럽트 불가
* 공정성: 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없다. 최악의 경우 스레드가 너무 오랜기간 락을 획득하지 못할 수 있다.

조금 더 정교한 설정이 필요하면 java.util.concurrent 동시성 문제 해결을 위한 패키지를 사용한다.



## 2. concurrent.Lock

java.util.concurrent 라이브러리 패키지는 synchronized의 단점들을 보안하기 위해 만들어졌다.

### 2.1. LockSupport

`LockSupport`는 기본적인 스레드 동기화 메커니즘을 제공하는 클래스입니다. 이 클래스는 낮은 레벨의 잠금을 처리하며 스레드를 잠들게 하거나 깨우는 메서드를 제공합니다.&#x20;

`LockSupport`의 주요 기능들입니다:

* `park()`: 현재 스레드를 WAITING 상태로 만듭니다. 다른 스레드에 의해 깨워질 때까지 멈춰 있습니다.
* `unpark(Thread thread)`: 지정된 스레드를 WAITING 상태에서 RUNNABLE로 변경
* `parkNanos(long nanos)`: 주어진 시간(나노 초) 동안 스레드를 TIMED\_WAITING상태로 변경합니다. 시간이 완료되면 자동으로 깨어납니다.
* `parkUntil(long deadline)`: 지정된 시점까지 스레드를 멈춥니다. 이 기능은 주로 타임스탬프 기반으로 스레드를 일시 중지하고 싶을 때 유용합니다.

```java
package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class LockSupportMainV1 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ParkTask(), "Thread-1");
        thread1.start();

        // 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다.
        sleep(100);
        log("Thread-1 state: " + thread1.getState());

        log("main -> unpark(Thread-1)");
        LockSupport.unpark(thread1); // 1. unpark 사용
        //thread1.interrupt();  // 2. interrupt() 사용
    }

    static class ParkTask implements Runnable {
        @Override
        public void run() {
            log("park 시작");
            LockSupport.park();
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}

```

```
// 실행 결과
14:29:32:062 [ Thread-1] park 시작
14:29:32:150 [     main] Thread-1 state: WAITING
14:29:32:151 [     main] main -> unpark(Thread-1)
14:29:32:151 [ Thread-1] park 종료, state: RUNNABLE
14:29:32:155 [ Thread-1] 인터럽트 상태: false
```

* main 스레드가 Thread-1을 start() 하면 Thread-1은 RUNNABLE 상태가 된다.
* Thread-1은 Thread-park()를 호출. Thread-1은 RUNNABLE -> WAITING 상태가 된다.
* main 스레드가 Thread-1을 깨운다. Thread-1은 대기 상태에서 실행 가능 상태로 변한다. WAITING -> RUNNABLE
* park()와 unpark()의 차이는, park는 해당 스레드가 실행하고, unpark는 다른 스레드에 의해서 실행되어야한다.

### 2.2. Interrupt 사용

```
14:32:31:506 [ Thread-1] park 시작
14:32:31:580 [     main] Thread-1 state: WAITING
14:32:31:580 [     main] main -> unpark(Thread-1)
14:32:31:581 [ Thread-1] park 종료, state: RUNNABLE
14:32:31:586 [ Thread-1] 인터럽트 상태: true
```

LockSupport.unpark(thread1) 대신, thread1.interrupt()를 사용하게되어도 깨울 수 있지만 인터럽트 상태가 true인걸 확인할 수 있다.

### 2.3. 시간 대기

parkNanos(nanos)를 사용한 경우.

```java
package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class LockSupportMainV2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ParkTask(), "Thread-1");
        thread1.start();

        // 잠시 대기하여 thread1이 park 상태에 빠질 시간을 준다.
        sleep(100);
        log("Thread-1 state: " + thread1.getState());
    }

    static class ParkTask implements Runnable {
        @Override
        public void run() {
            log("park 시작, 2초 대기");
            LockSupport.parkNanos(2000_000000); // parkNanos 사용
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}
```

```
14:34:49:069 [ Thread-1] park 시작, 2초 대기
14:34:49:149 [     main] Thread-1 state: TIMED_WAITING
14:34:51:084 [ Thread-1] park 종료, state: RUNNABLE
14:34:51:088 [ Thread-1] 인터럽트 상태: false
```

* 따로 unpark를 해주지 않아도, 2초후에 꺠어난다.



BLOCKED vs WAITING

WAITING 상태에 특정한 시간까지만 대기하는 기능이 포함된 것이 TIMED\_WAITING. 둘을 묶어서 WAITING 상태라고 가정.

* 인터럽트
  * BLOCKED 상태는 인터럽트가 걸려도 대기 상태를 빠져나오지 못한다. 여전히 BLOCKED 상태
  * WAITING, TIMED\_WAITING 상태는 인터럽트가 걸리면 대기 상태를 빠져 나온다. RUNNABLE 상태로 변경.
* 용도
  * BLOCKED 상태: 자바의 synchronized에서 락을 획득하기 위해 대기할 때 사용.
  * WAITING, TIMED\_WAITING 상태: 스레드가 특정 조건이나 시간 동안 대기할 때 발생하는 상태
  * WAITING 상태: 다양한 상황에서 사용된다. 예를 들어, Thread.join(), LockSupport.park(), Object.wait()과 같은 메서드 호출시 WAITING 상태가 된다.
  * TIMED\_WAITING 상태: Thread.sleep(ms), Object.wait(long timeout), Thread.join(long millis), LockSupport.parkNanos(ns) 등과 같은 시간 제한이 있는 대기 메서드 호출시 발생.
* Thread.join() <--> Thread.join(millis)
* Thread.park() <--> Thread.parkNanos(long millis)
* Object.wait() <--> Object.wait(long timeout)

이러한 기능을 직접 구현하기는 어렵다. 여러 스레드가 대기하고 있는 상태에서 다음 스레드를 선정하는 것, 우선순위를 부여하는 것 등의 세밀한 작업은 LockSupport로 하기에는 synchronized 보다 더 저수준의 기능이다. 이를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 존재한다.



## 3. Lock 인터페이스와 ReentrantLock

### 3.1. Lock 인터페이스

자바1.0 Synchronized와 BLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해 자바1.5부터 Lock 인터페이스와 ReentrantLock 구현체가 등장했다.

Lock 인터페이스를 통해서 synchronized의 단점인 **무한 대기 문제 해결**

**여기서의 Lock은 객체 내부에 있는 모니터 락이랑 다른 락이다. Lock 인터페이스와 ReentrantLock이 제공하는 기능이다.**



Lock 인터페이스는 아래와 같다:

```java
package java.util.concurrent.locks;

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

* void lock()
  * 락을 획득. 만약 다른 스레드가 이미 락을 획득 했다면, 락이 풀릴때까지 대기(WAITING) 한다. 인터럽트에 응답하지 않음
  * 맛집에 줄을 서면 끝까지 기다린다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기하지 않고 기다림.
  * **WAITING 상태의 스레드에 인터럽트가 발생하면 WAITING 상태를 빠져나오는게 정상. 근데 lock()의 경우에는 다시 WAITING으로 변경시킨다. 결국 인터럽트를 무시**
* void lockInterruptibly()
  * 락 획득을 시도하되, 다른 스레드가 인터럽트 할 수 있다. 락을 획득할 수 없다면 획득할때까지 대기한다. 대기중 인터럽트가 발생하면 InterruptException이 발생하며 락 획득을 포기
  * 맛집에 줄을 서서 기다린다. 친구가 다른 맛집을 찾았다고 중간에 연락하면 포기한다.
* boolean tryLock()
  * 락 획득을 시도하고, 즉시 성공 여부 반환. 다른 스레드가 이미 락을 획득하면 false를 반환
  * true 반환 -> 락 획득 -> 임계 구역 코드 실행
  * false 반환 -> 포기
  * 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기
* boolean tryLock(long time, TimeUnit unit)
  * 주어진 시간동안 락 획득 시도. 주어진 시간 안에 락을 획득하면 true 반환. 주어진 시간이 지나도 락을 획득하지 못한 경우 false를 반환.
  * 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득 포기
  * 맛집에 줄을 서지만 특정 시간 만큼만 기다린다. 특정 시간이 지난 후에도 계속 줄을 서야 하면 포기. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기
* Condition newCondition()
  * Condition 객체를 생성하여 반환. Condition 객체는 락과 결합되어 사용되며, 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 한다. Object 클래스의 wait, notify, notifyAll 메서드와 유사함



### 3.2. ReentrantLock - 이론

공정성을 해결하기 위해 사용된다. Lock 인터페이스의 구현체 중 가장 대표적. 공정하게 스레드가 락을 얻을 수 있도록 한다.

````java
// 사용 예시
package thread.sync.locks.reentrantLock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockEx {
    // 비공정 모드 락
    private final Lock nonFairLock = new ReentrantLock();
    // 공정 모드 락
    private final Lock fairLock = new ReentrantLock(true);
    public void nonFairLockTest() {
        nonFairLock.lock();
        try {
            // 임계 영역
        } finally {
            nonFairLock.unlock();
        }
    }
    public void fairLockTest() {
        fairLock.lock();
        try {
            // 임계 영역
        } finally {
            fairLock.unlock();
        }
    }
}
```

````

ReentrantLock은 공정성(fairness) 모드와 비공정성(non-fair) 모드로 설정 가능.

* 공정 모드(fair mode)
  * 생성자에 true를 전달.
  * 락을 요청한 순서대로 스레드가 락을 획득할 수 있게 한다. FIFO 방식으로 공정성을 보장하지만 성능 저하
* 비공정 모드(non-fair mode)
  * 공정모드보다 락을 획득하는 속도가 빠르다
  * 새로운 스레드가 기존 대기 스레드보다 먼저 락을 획득할 수 있다.
  * starvation이 발생할 수도 있다 (다만 내부적으로는 queue가 사용되어서, 정확히 락을 반환한 시점에 새로운 스레드가 오지 않는 이상 크게 문제는 없다고 함)
* 정리: 비공정 모드 vs 공정모드 - 성능 중시 vs 순서 보장

### 3.3. ReentrantLock - 활용

이를 출금 예제에 활용해본다.

```java
package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV4 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

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

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

        lock.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(); // ReentrantLock을 사용할 때는 반드시 unlock()을 호출해야 한다.
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }

    }
}
```

```
// 실행 결과
17:10:50:919 [       t1] 거래 시작: BankAccountV4
17:10:50:919 [       t2] 거래 시작: BankAccountV4
17:10:50:929 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
17:10:50:930 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
17:10:51:397 [     main] t1 state: TIMED_WAITING
17:10:51:397 [     main] t2 state: WAITING
17:10:51:945 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
17:10:51:945 [       t1] 거래 종료
17:10:51:946 [       t2] [검증 시작] 출금액: 800, 잔액: 200
17:10:51:946 [       t2] [검증 실패] 출금액: 800, 잔액: 200
17:10:51:949 [     main] 최종 잔액: 200
```

* private final Lock lock = new ReentrantLock()을 사용하도록 선언
* 임계 영역이 끝나면 반드시 락을 반납해야한다. lock.unlock()은 반드시 finally에 작성.
* t2는 WAITING 상태이다. BLOCKED 상태는 synchronized에서만 사용된다.

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FYvX8ZFavDyUcBZSXuDwR%2Fimage.png?alt=media&#x26;token=973bb6cf-ed31-4727-80db-b7f17f667431" alt=""><figcaption></figcaption></figure>

* t2는 락을 획득하지 못해서 x002 대기큐에 들어가게 된다(이때 락은 모니터 락이 아님)

### 3.4. ReentrantLock - 대기 중단

ReentrantLock을 사용하면 락을 무한 대기하지 않고, 중간에 빠져나오는게 가능. 락을 얻을 수 없다면 즉시 빠져나오기도 가능.

* boolean tryLock()&#x20;
  * 락 획득을 시도하고 즉시 성공 여부를 반환. 만약 다른 스레드가 이미 락을 획득했다면 false 반환. 그렇지 않으면 락을 획득하고 true 반환.
  * 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기
* boolean tryLock(long time, TimeUnit unit)
  * 주어진 시간 동안 락 획득을 시도한다. 주어진 시간 안에 락을 획득하면 true 반환. 주어진 시간이 지나도 락을 획득하지 못한 경우 false를 반환. 대기중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득을 포기한다.
  * 맛집에 줄을 서지만 특정 시간 만큼 기다린다. 특정 시간이 지나도 계속 줄을 서야 한다면 포기한다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기한다.

**tryLock 예시**

```java
package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV5 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

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

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

        if (!lock.tryLock()) {
            log("[진입 실패] 이미 처리중인 작업이 있습니다.");
            return false;
        }
        
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            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();
        }

    }
}
```

```
// 실행 결과
17:18:47:025 [       t1] 거래 시작: BankAccountV5
17:18:47:025 [       t2] 거래 시작: BankAccountV5
17:18:47:029 [       t2] [진입 실패] 이미 처리중인 작업이 있습니다.
17:18:47:036 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
17:18:47:503 [     main] t1 state: TIMED_WAITING
17:18:47:503 [     main] t2 state: TERMINATED
17:18:48:051 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
17:18:48:051 [       t1] 거래 종료
17:18:48:054 [     main] 최종 잔액: 200
```

* t2의 상태가 TERMINATED인것을 볼 수 있다.



**tryLock(시간) 예시**

```java
package thread.sync;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV6 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV6(int balance) {
        this.balance = balance;
    }

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

        try {
            if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                log("[진입 실패] 이미 처리중인 작업이 있습니다.");
                return false;
            }
        } catch (InterruptedException e) {
            log("[진입 실패] 다른 스레드에 의해 인터럽트 되었습니다.");
            return false;
        }


        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            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();
        }

    }
}
```

```
// 실행 결과
17:22:01:143 [       t2] 거래 시작: BankAccountV6
17:22:01:143 [       t1] 거래 시작: BankAccountV6
17:22:01:154 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
17:22:01:620 [     main] t1 state: TIMED_WAITING
17:22:01:620 [     main] t2 state: TIMED_WAITING
17:22:01:651 [       t1] [진입 실패] 이미 처리중인 작업이 있습니다.
17:22:02:161 [       t2] [출금 완료] 출금액: 800, 변경 잔액: 200
17:22:02:161 [       t2] 거래 종료
17:22:02:165 [     main] 최종 잔액: 200
```

* t1는 TIMED\_WAITING 상태로 있다가, TERMINATED가 된다.
* 0.5초간 락을 획득하지 못함으로 false를 반환하게 된다.
