diff --git a/build.gradle.kts b/build.gradle.kts index cdbecfb..2f10496 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/test/java/example/concurrency/ch13/DistributedLockTest.java b/src/test/java/example/concurrency/ch13/DistributedLockTest.java new file mode 100644 index 0000000..c0e815e --- /dev/null +++ b/src/test/java/example/concurrency/ch13/DistributedLockTest.java @@ -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(); + + } +} diff --git a/study/ch13.md b/study/ch13.md new file mode 100644 index 0000000..a8cdaff --- /dev/null +++ b/study/ch13.md @@ -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 + +``` \ No newline at end of file