Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2:2.2.224")

implementation("org.redisson:redisson-spring-boot-starter:3.24.3")
implementation("org.springframework.boot:spring-boot-starter-actuator")

testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand Down
59 changes: 59 additions & 0 deletions src/test/java/example/concurrency/ch13/DistributedLockTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package example.concurrency.ch13;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class DistributedLockTest {

@Test
void lockTest() throws Exception {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(24)
.setConnectTimeout(10000)
.setTimeout(3000)
.setRetryAttempts(4)
.setRetryInterval(1500);

RedissonClient client = Redisson.create(config);

RLock lock = client.getLock("test-lock");
lock.tryLock(3, 3, TimeUnit.SECONDS);
CountDownLatch startLatch = new CountDownLatch(5);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int threadIndex = i;
executorService.submit(() -> {
try {
System.out.println("Thread-" + threadIndex + " trying to acquire lock...");
lock.lock();
System.out.println("Thread-" + threadIndex + " acquired lock.");
// Critical section
Thread.sleep(2000); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
startLatch.countDown();
try {
lock.unlock();
} catch (Exception e) {
System.out.println("Thread-" + threadIndex + " failed to release lock: " + e.getMessage());
}
System.out.println("Thread-" + threadIndex + " released lock.");
}
});
}
startLatch.await();

}
}
184 changes: 184 additions & 0 deletions study/ch13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# 명시적인 락

- **13.1 Lock과 ReentrantLock**
- 13.1.1 폴링과 시간 제한이 있는 락 확보 방법
- 13.1.2 인터럽트 걸 수 있는 락 확보 방법
- 13.1.3 블록을 벗어나는 구조의 락

- **13.2 성능에 대한 고려 사항**
- **13.3 공정성**
- **13.4 synchronized 또는 ReentrantLock 선택**
- **13.5 읽기-쓰기 락**
- **분산락**

---

# 13.1 Lock과 ReentrantLock

**ReentrantLock 은 Synchronized 에 비하여, 락을 제대로 확보하기 어려운 시점에 훨씬 능동적으로 대처할 수 있다. (p406)**

- Timeout 을 지정하여 유연하게 대처 가능

``` java
public boolean tryLock(long timeout, TimeUnit unit)

LockSupport.parkNanos(this, nanos);
```

하지만 정확한 시간초 뒤에 깨우는게 아니다. 약간의 오차 발생 가능 그래서 루프를 돌면서 확인해야 함

---

**왜 명시적인 락이 필요할까?**

- 대기 상태에 들어가지 않으면서 락을 확보하는 방법이 필요
- tryLock()
- 락을 확보하는데 시간이 오래 걸릴 수 있는 상황에서, 타임아웃을 지정하여 대기 시간을 제한할 수 있어야 함
- tryLock(timeout, unit)
- 하지만 finally 블록에서 반드시 해제해야 함

---

## 13.1.1 폴링과 시간 제한이 있는 락 확보 방법

두가지 방식의 핵심은 락을 획득하려는 시도 뒤에 통제권을 얻을 수 있다는 것이다.

- tryLock() : 즉시 반환 -> 폴링 방식으로 활용 가능
- tryLock(timeout, unit) : 지정된 시간 동안 락을 얻기 위해 대기 -> 타임아웃 방식으로 활용 가능

---

## 13.1.2 인터럽트 걸 수 있는 락 확보 방법

- lockInterruptibly() : 락을 얻기 위해 대기하는 동안 인터럽트가 걸리면, InterruptedException 발생
- tryLock(timeout, unit) : 지정된 시간 동안 락을 얻기 위해 대기하는 동안 인터럽트가 걸리면, InterruptedException 발생

두 메서드 모두 Thread 가 interrupted 상태인지 확인 후 인터럽트된 상태 일 경우 `acquire` 메서드에서 음수 반환 그리고, 이를 호출한 쓰레드에서 음수 판단 후 예외 발생

왜 쓸까? -> 처음에 Timeout 시간을 정하기 애매하고, 특정 트리거를 받아서 lock 대기를 해제하고 싶을 때?

---

## 13.1.3 블록을 벗어나는 구조의 락

synchronized 는 진입 시 락을 획득하고 블록을 벗어날 때 자동으로 락을 해제하는 구조
-> 락 해제에 대한 실수를 방지해 준다.

하지만, 복잡한 프로그램에서는 좀 더 유연한 구조의 lock 획득과 해제가 필요하다.
-> hash collection 같은 경우 여러 개의 해시 블록을 구성하여 각각의 블록마다 락을 유연하게 거는 구조이다.

---

# 13.2 성능에 대한 고려 사항

- 자바 5까지만 해도 성능적 측면에서, ReentrantLock > synchronized 이다. 특히, 스레드 개수가 늘어날 수록 성능차이는 심해진다.
- 자바 6부터 JVM 에서 synchronized 를 최적화 하면서, 두 방식의 성능차이는 거의 없어졌다.
- 교훈: `X 가 Y 보다 더 빠르다` 라는 명제는 그다지 오래 가지 못한다.

---

# 13.3 공정성

- `ReentrantLock` 의 설정 방식은 두 가지가 있다.
- 공정한 락 (fair lock) : 가장 오래 기다린 스레드가 가장 먼저 락을 획득
- 불공정한 락 (unfair lock) : 락을 기다리는 순서와 상관없이, 락이 해제되면 바로 획득 시도
``` java
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
```
- 두 방식 모두 Queue 에서 대기하는 것은 동일, 하지만 불공정락은 처음 락을 획득하려는 시도를 한다.
``` java
불공정 락 (NonfairSync)
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { -> 냅다 락을 획득 시도
setExclusiveOwnerThread(current);
return true;
}
...
}

공정 락 (FairSync)
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { -> 락 누가 쓰고있는지 봄
if (!hasQueuedThreads() && compareAndSetState(0, 1)) { -> 대기중인 스레드가 없으면 락 획득 시도
setExclusiveOwnerThread(current);
return true;
}
}
...
}
```

- 언제 공정한 락 / 비공정한 락을 써야될까?
- (1). 락을 얻으려했던 시점의 순서가 락을 획득하려했던 시점의 순서와 일치해야하는 경우
- (2). 락을 점유하고, 그 후의 실행 task 가 오래 걸리는 경우 -> cas 알고리즘으로 락 획득 시도를 하는 것이 오히려 낭비가 될 수 있다.
- 그 외에는 비공정락이 성능적으로 좋기 때문에 사용할 것 같다.
- 왜 비공정 락이 성능적으로 좋을까?
- 공정락의 획득 순서를 보자.
- ```
1. T1: 락 점유중
2. T2: 락 획득 시도 -> 실패 -> 대기 큐에 들어감
3. T1: 락 해제 -> T2 이 대기 큐에서 가장 오래 기다렸으므로, T2 이 락 획득
```
- 여기서 T1 이 락을 해제하고 T2 깨어나 다시 락을 획득하기 까지 시간이 걸린다. 이 간격의 손실이 있다.
- 하지만 비공정 락은 해당 간격 사이에 T3 가 들어와서 락을 획득할 수 있다. (처리량 증가)

---

# 13.4 synchronized 또는 ReentrantLock 선택

| 기능 | synchronized | Lock |
|-----------|---------------------|-----------------------|
| **기본 락** | `synchronized(obj)` | `lock.lock()` |
| **인터럽트** | 불가능 | `lockInterruptibly()` |
| **타임아웃** | 불가능 | `tryLock(time, unit)` |
| **조건 변수** | 1개 (wait/notify) | 여러 개 (Condition) |
| **공정성** | 불공정 | 공정/불공정 선택 |
| **성능** | JVM 최적화 | 유연성 높음 |

자바 5에는 synchronized 가 성능이 떨어졌지만, 어느 곳에서 블록됐는지 모니터링할 수 있었다. (ReentrantLock 은 불가)
하지만, 자바 6부터 ReentrantLock 도 모니터링이 가능해졌다.
``` java
LockSupport.park(this);
```
this 는 쓰레드를 대기하도록 한 주체인데, blocker 라고 칭한다.
그냥 ReentrantLock 을 쓸 것 같다.

---

# 13.5 읽기-쓰기 락
ReentrantLock 은 하나의 스레드만이 락을 확보할 수 있다.
너무 엄격한거 아닌가?
-> ReentrantReadWriteLock
Read / Write 락을 따로 관리
- Write 락 있을 시 Read 락 못 얻음
- Read 락 있을 시 Write 락 못 얻음
- Read 락 여러 개 가능
-> MySql 의 공유 락 / 베타 락 같은 느낌인 듯 함 (for Share / for Update)

---

# 분산락 (추가)
Multi instance 에서 Lock 을 걸어야 하는 상황이 발생할 수 있다.
Redisson 을 이용하여 분산락을 구현할 수 있다.
LuaScript 를 이용하여 원자적으로 처리할 수 있다.

``` java
불공정락
client.getLock() // pub/sub + broadcast

공정락
client.getFairLock() // pub/sub + FIFO

스핀락
client.getSpinLock() // polling + backoff strategy

```