Skip to content

Commit 883be89

Browse files
authored
Study/week11 (#7)
1 parent 3db8507 commit 883be89

File tree

3 files changed

+244
-1
lines changed

3 files changed

+244
-1
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies {
2222
implementation("org.springframework.boot:spring-boot-starter-web")
2323
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
2424
runtimeOnly("com.h2database:h2:2.2.224")
25-
25+
implementation("org.redisson:redisson-spring-boot-starter:3.24.3")
2626
implementation("org.springframework.boot:spring-boot-starter-actuator")
2727

2828
testImplementation("org.springframework.boot:spring-boot-starter-test")
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package example.concurrency.ch13;
2+
3+
4+
import java.util.concurrent.CountDownLatch;
5+
import java.util.concurrent.ExecutorService;
6+
import java.util.concurrent.Executors;
7+
import java.util.concurrent.TimeUnit;
8+
import org.junit.jupiter.api.Test;
9+
import org.redisson.Redisson;
10+
import org.redisson.api.RLock;
11+
import org.redisson.api.RedissonClient;
12+
import org.redisson.config.Config;
13+
14+
public class DistributedLockTest {
15+
16+
@Test
17+
void lockTest() throws Exception {
18+
Config config = new Config();
19+
config.useSingleServer()
20+
.setAddress("redis://localhost:6379")
21+
.setConnectionPoolSize(64)
22+
.setConnectionMinimumIdleSize(24)
23+
.setConnectTimeout(10000)
24+
.setTimeout(3000)
25+
.setRetryAttempts(4)
26+
.setRetryInterval(1500);
27+
28+
RedissonClient client = Redisson.create(config);
29+
30+
RLock lock = client.getLock("test-lock");
31+
lock.tryLock(3, 3, TimeUnit.SECONDS);
32+
CountDownLatch startLatch = new CountDownLatch(5);
33+
ExecutorService executorService = Executors.newFixedThreadPool(5);
34+
for (int i = 0; i < 5; i++) {
35+
final int threadIndex = i;
36+
executorService.submit(() -> {
37+
try {
38+
System.out.println("Thread-" + threadIndex + " trying to acquire lock...");
39+
lock.lock();
40+
System.out.println("Thread-" + threadIndex + " acquired lock.");
41+
// Critical section
42+
Thread.sleep(2000); // Simulate work
43+
} catch (InterruptedException e) {
44+
e.printStackTrace();
45+
} finally {
46+
startLatch.countDown();
47+
try {
48+
lock.unlock();
49+
} catch (Exception e) {
50+
System.out.println("Thread-" + threadIndex + " failed to release lock: " + e.getMessage());
51+
}
52+
System.out.println("Thread-" + threadIndex + " released lock.");
53+
}
54+
});
55+
}
56+
startLatch.await();
57+
58+
}
59+
}

study/ch13.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# 명시적인 락
2+
3+
- **13.1 Lock과 ReentrantLock**
4+
- 13.1.1 폴링과 시간 제한이 있는 락 확보 방법
5+
- 13.1.2 인터럽트 걸 수 있는 락 확보 방법
6+
- 13.1.3 블록을 벗어나는 구조의 락
7+
8+
- **13.2 성능에 대한 고려 사항**
9+
- **13.3 공정성**
10+
- **13.4 synchronized 또는 ReentrantLock 선택**
11+
- **13.5 읽기-쓰기 락**
12+
- **분산락**
13+
14+
---
15+
16+
# 13.1 Lock과 ReentrantLock
17+
18+
**ReentrantLock 은 Synchronized 에 비하여, 락을 제대로 확보하기 어려운 시점에 훨씬 능동적으로 대처할 수 있다. (p406)**
19+
20+
- Timeout 을 지정하여 유연하게 대처 가능
21+
22+
``` java
23+
public boolean tryLock(long timeout, TimeUnit unit)
24+
25+
LockSupport.parkNanos(this, nanos);
26+
```
27+
28+
하지만 정확한 시간초 뒤에 깨우는게 아니다. 약간의 오차 발생 가능 그래서 루프를 돌면서 확인해야 함
29+
30+
---
31+
32+
**왜 명시적인 락이 필요할까?**
33+
34+
- 대기 상태에 들어가지 않으면서 락을 확보하는 방법이 필요
35+
- tryLock()
36+
- 락을 확보하는데 시간이 오래 걸릴 수 있는 상황에서, 타임아웃을 지정하여 대기 시간을 제한할 수 있어야 함
37+
- tryLock(timeout, unit)
38+
- 하지만 finally 블록에서 반드시 해제해야 함
39+
40+
---
41+
42+
## 13.1.1 폴링과 시간 제한이 있는 락 확보 방법
43+
44+
두가지 방식의 핵심은 락을 획득하려는 시도 뒤에 통제권을 얻을 수 있다는 것이다.
45+
46+
- tryLock() : 즉시 반환 -> 폴링 방식으로 활용 가능
47+
- tryLock(timeout, unit) : 지정된 시간 동안 락을 얻기 위해 대기 -> 타임아웃 방식으로 활용 가능
48+
49+
---
50+
51+
## 13.1.2 인터럽트 걸 수 있는 락 확보 방법
52+
53+
- lockInterruptibly() : 락을 얻기 위해 대기하는 동안 인터럽트가 걸리면, InterruptedException 발생
54+
- tryLock(timeout, unit) : 지정된 시간 동안 락을 얻기 위해 대기하는 동안 인터럽트가 걸리면, InterruptedException 발생
55+
56+
두 메서드 모두 Thread 가 interrupted 상태인지 확인 후 인터럽트된 상태 일 경우 `acquire` 메서드에서 음수 반환 그리고, 이를 호출한 쓰레드에서 음수 판단 후 예외 발생
57+
58+
왜 쓸까? -> 처음에 Timeout 시간을 정하기 애매하고, 특정 트리거를 받아서 lock 대기를 해제하고 싶을 때?
59+
60+
---
61+
62+
## 13.1.3 블록을 벗어나는 구조의 락
63+
64+
synchronized 는 진입 시 락을 획득하고 블록을 벗어날 때 자동으로 락을 해제하는 구조
65+
-> 락 해제에 대한 실수를 방지해 준다.
66+
67+
하지만, 복잡한 프로그램에서는 좀 더 유연한 구조의 lock 획득과 해제가 필요하다.
68+
-> hash collection 같은 경우 여러 개의 해시 블록을 구성하여 각각의 블록마다 락을 유연하게 거는 구조이다.
69+
70+
---
71+
72+
# 13.2 성능에 대한 고려 사항
73+
74+
- 자바 5까지만 해도 성능적 측면에서, ReentrantLock > synchronized 이다. 특히, 스레드 개수가 늘어날 수록 성능차이는 심해진다.
75+
- 자바 6부터 JVM 에서 synchronized 를 최적화 하면서, 두 방식의 성능차이는 거의 없어졌다.
76+
- 교훈: `X 가 Y 보다 더 빠르다` 라는 명제는 그다지 오래 가지 못한다.
77+
78+
---
79+
80+
# 13.3 공정성
81+
82+
- `ReentrantLock` 의 설정 방식은 두 가지가 있다.
83+
- 공정한 락 (fair lock) : 가장 오래 기다린 스레드가 가장 먼저 락을 획득
84+
- 불공정한 락 (unfair lock) : 락을 기다리는 순서와 상관없이, 락이 해제되면 바로 획득 시도
85+
``` java
86+
public ReentrantLock() {
87+
sync = new NonfairSync();
88+
}
89+
90+
public ReentrantLock(boolean fair) {
91+
sync = fair ? new FairSync() : new NonfairSync();
92+
}
93+
```
94+
- 두 방식 모두 Queue 에서 대기하는 것은 동일, 하지만 불공정락은 처음 락을 획득하려는 시도를 한다.
95+
``` java
96+
불공정 락 (NonfairSync)
97+
final boolean initialTryLock() {
98+
Thread current = Thread.currentThread();
99+
if (compareAndSetState(0, 1)) { -> 냅다 락을 획득 시도
100+
setExclusiveOwnerThread(current);
101+
return true;
102+
}
103+
...
104+
}
105+
106+
공정 락 (FairSync)
107+
final boolean initialTryLock() {
108+
Thread current = Thread.currentThread();
109+
int c = getState();
110+
if (c == 0) { -> 락 누가 쓰고있는지 봄
111+
if (!hasQueuedThreads() && compareAndSetState(0, 1)) { -> 대기중인 스레드가 없으면 락 획득 시도
112+
setExclusiveOwnerThread(current);
113+
return true;
114+
}
115+
}
116+
...
117+
}
118+
```
119+
120+
- 언제 공정한 락 / 비공정한 락을 써야될까?
121+
- (1). 락을 얻으려했던 시점의 순서가 락을 획득하려했던 시점의 순서와 일치해야하는 경우
122+
- (2). 락을 점유하고, 그 후의 실행 task 가 오래 걸리는 경우 -> cas 알고리즘으로 락 획득 시도를 하는 것이 오히려 낭비가 될 수 있다.
123+
- 그 외에는 비공정락이 성능적으로 좋기 때문에 사용할 것 같다.
124+
- 왜 비공정 락이 성능적으로 좋을까?
125+
- 공정락의 획득 순서를 보자.
126+
- ```
127+
1. T1: 락 점유중
128+
2. T2: 락 획득 시도 -> 실패 -> 대기 큐에 들어감
129+
3. T1: 락 해제 -> T2 이 대기 큐에서 가장 오래 기다렸으므로, T2 이 락 획득
130+
```
131+
- 여기서 T1 이 락을 해제하고 T2 깨어나 다시 락을 획득하기 까지 시간이 걸린다. 이 간격의 손실이 있다.
132+
- 하지만 비공정 락은 해당 간격 사이에 T3 가 들어와서 락을 획득할 수 있다. (처리량 증가)
133+
134+
---
135+
136+
# 13.4 synchronized 또는 ReentrantLock 선택
137+
138+
| 기능 | synchronized | Lock |
139+
|-----------|---------------------|-----------------------|
140+
| **기본 락** | `synchronized(obj)` | `lock.lock()` |
141+
| **인터럽트** | 불가능 | `lockInterruptibly()` |
142+
| **타임아웃** | 불가능 | `tryLock(time, unit)` |
143+
| **조건 변수** | 1개 (wait/notify) | 여러 개 (Condition) |
144+
| **공정성** | 불공정 | 공정/불공정 선택 |
145+
| **성능** | JVM 최적화 | 유연성 높음 |
146+
147+
자바 5에는 synchronized 가 성능이 떨어졌지만, 어느 곳에서 블록됐는지 모니터링할 수 있었다. (ReentrantLock 은 불가)
148+
하지만, 자바 6부터 ReentrantLock 도 모니터링이 가능해졌다.
149+
``` java
150+
LockSupport.park(this);
151+
```
152+
this 는 쓰레드를 대기하도록 한 주체인데, blocker 라고 칭한다.
153+
그냥 ReentrantLock 을 쓸 것 같다.
154+
155+
---
156+
157+
# 13.5 읽기-쓰기 락
158+
ReentrantLock 은 하나의 스레드만이 락을 확보할 수 있다.
159+
너무 엄격한거 아닌가?
160+
-> ReentrantReadWriteLock
161+
Read / Write 락을 따로 관리
162+
- Write 락 있을 시 Read 락 못 얻음
163+
- Read 락 있을 시 Write 락 못 얻음
164+
- Read 락 여러 개 가능
165+
-> MySql 의 공유 락 / 베타 락 같은 느낌인 듯 함 (for Share / for Update)
166+
167+
---
168+
169+
# 분산락 (추가)
170+
Multi instance 에서 Lock 을 걸어야 하는 상황이 발생할 수 있다.
171+
Redisson 을 이용하여 분산락을 구현할 수 있다.
172+
LuaScript 를 이용하여 원자적으로 처리할 수 있다.
173+
174+
``` java
175+
불공정락
176+
client.getLock() // pub/sub + broadcast
177+
178+
공정락
179+
client.getFairLock() // pub/sub + FIFO
180+
181+
스핀락
182+
client.getSpinLock() // polling + backoff strategy
183+
184+
```

0 commit comments

Comments
 (0)