ReentrantLock
ReentrantLock이란 객체에 적용된 synchronized 키워드처럼 동작한다.
아래의 코드처럼 Lock 객체를 사용해 동기화 환경을 만드는 것과 유사한 방식을 사용한다.
Object lock = new Object();
static void operation() {
synchronized (lock) {
...
}
}
하지만 ReentrantLock 을 사용한다면 더 많은 기능을, 더 구체적으로 사용할 수 있다.
그 이유는 ReentrantLock을 사용한다면 락의 공정성 을 보장할 수 있기 때문이다.
락의 공정성
락의 공정성이란, 멀티스레드 환경에서 여러 스레드가 특정 자원에 접근하기 위해 락을 요청할 때, 그 자원을 스레드의 도착 순서에 따라 공정하게 할당하는 성질을 의미한다.
ReentrantLock은 Queue를 사용해 선입선출 형태로 락이 할당되지만 synchronized 은 JVM 내부의 모니터 대기 집합이라는 곳에 스레드를 보관해놨다가 무작위로 선별하여 할당한다.
그렇기 때문에 ReentrantLock을 사용한다면 스레드의 기아상태를 방지할 수 있다.
참고로 기아상태란, 스레드가 락을 할당받지 못해 계속해서 대기만 하는 상태를 의미한다.
ReentrantLock 쿼리 메서드
- getQueuedThreads() : 락을 기다리고 있는 스레드 목록 리턴
- getOwner() : 락을 가지고 있는 스레드 리턴
- isHeldByCurrentThread() : 현재 스레드에 락이 있는지 확인
- isLocked() : 다른 스레드에 락이 있는지 없는지 확인
ReentrantLock 일반 메서드
- lockInterruptibly() : 스레드가 락을 대기하는 중에 인터럽트(interrupt)로 대기 상태에서 빠져나올 수 있게 하는 메서드
ReentrantLock.tryLock()
tryLock() 메서드는 ReentrantLock 에서 가장 중요한 메서드이다.
lockObject.lock();
try {
useShareResources();
}
finally {
lock.unlock();
}
만약 tryLock()을 사용하지 않고, 락을 다른 스레드가 사용하고 있는 상황에 락 획득을 시도한다면 위 코드의 lockObject.lock() 부분에서 계속 대기하게 되는 것이다.
if (lock.tryLock()) {
try {
useShareResources();
}
finally {
lock.unlock();
}
} else {
// 락을 획득하지 못한 경우
}
하지만 tryLock() 은 락 객체가 있다면 락 객체를 얻은 후, true을 리턴하여 작업을 계속해서 진행한다. 만약 락 객체를 얻지 못한 경우엔 스레드가 대기하는 대신, false를 리턴하고 다음 명령으로 넘어간다.
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
useShareResources();
}
finally {
lock.unlock();
}
} else {
// 락을 획득하지 못한 경우
}
아니면 바로 else 부분으로 넘어가는 것이 아니라 약간의 대기시간을 가졌으면 하는 경우에는 tryLock() 메서드의 파라미터에 대기 시간을 설정하면, 저 대기시간만큼 대기하고 재요청하여 락 객체를 획득할 수 있다.
ReentrantReadWriteLock
ReentrantReadWriteLock란 읽기 락과 쓰기 락을 합쳐서 하나의 락으로 만든 것이다.
이렇게 읽는 락과 쓰는 락을 나눈 이유는, 실제 구현된 기능에선 조회만 담당하는 메서드도 상당히 많기 때문이다.
읽기 작업만 수행하는 스레드는 쓰기 작업을 하지 않기 때문에, 여러 스레드가 동시에 데이터를 읽어도 충돌이 발생하지 않는다. 그래서 이러한 작업에는 읽기 락을 사용하여 여러 스레드가 동시에 자원을 읽을 수 있도록 허용한다.
하지만 쓰기 락의 경우엔 이전의 방식과 동일하게 오직 하나의 스레드만 자원에 접근할 수 있도록 허용한다.
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock readLock = reentrantReadWriteLock.readLock();
readLock.lock(); // 읽기 락 시작
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
Lock writeLock = reentrantReadWriteLock.writeLock();
writeLock.lock(); // 쓰기 락 시작
이전에 만들어놓은 txt 파일을 읽어, txt파일에 특정 word의 갯수를 파악하는 코드를 작성했었는데, 여기다가 단순한 synchronized와 읽기 락의 성능을 비교해봤다.
countWords() 메서드에 synchronized을 사용한 경우
public class ReadTxtHttpServer {
static final int NUMBER_OF_THREAD = 7;
static final String FILE_PATH = "resources/war_and_peace.txt";
static String text = "";
static ReentrantReadWriteLock reentrantReadWriteLock;
public static void main(String[] args) throws IOException {
text = new String(Files.readAllBytes(Paths.get(FILE_PATH)));
reentrantReadWriteLock = new ReentrantReadWriteLock();
startServer();
}
...
private static class WordCountHandler implements HttpHandler {
...
private synchronized long countWords(String word) {
long count = 0;
int idx = 0;
while (idx >= 0) {
idx = text.indexOf(word, idx);
if (idx >= 0) {
count++;
idx++;
}
}
return count;
}
}
}
countWords() 메서드에 읽기 락을 사용한 경우
public class ReadTxtHttpServer {
static final int NUMBER_OF_THREAD = 7;
static final String FILE_PATH = "resources/war_and_peace.txt";
static String text = "";
static ReentrantReadWriteLock reentrantReadWriteLock;
public static void main(String[] args) throws IOException {
text = new String(Files.readAllBytes(Paths.get(FILE_PATH)));
reentrantReadWriteLock = new ReentrantReadWriteLock();
startServer();
}
...
private static class WordCountHandler implements HttpHandler {
...
private long countWords(String word) {
Lock readLock = reentrantReadWriteLock.readLock(); // 읽기 락 획득
readLock.lock(); // 읽기 락 실행
long count = 0;
int idx = 0;
try {
while (idx >= 0) {
idx = text.indexOf(word, idx);
if (idx >= 0) {
count++;
idx++;
}
}
} finally {
readLock.unlock();
}
return count;
}
}
}
synchronized 결과
- Average (평균 응답 시간) : 854
- Min (가장 빠른 응답 시간) : 62
- Max (가장 느린 응답 시간) : 1613
- Throughput (초당 처리량) : 383.3
읽기 락 결과
- Average : 99
- Min : 3
- Max : 247
- Throughput : 997.0
보다시피 읽기 락이 훨씬 좋은 성능을 낼 수 있다.
참조
https://www.udemy.com/course/java-multi-threading/?couponCode=24T6MT102824
'백엔드 > Java' 카테고리의 다른 글
[Java] 멀티스레딩 - 가상 스레드 (0) | 2024.11.04 |
---|---|
[Java] 멀티스레딩 - 스레드 간 통신 (2) | 2024.11.04 |
[Java] 멀티스레딩 - 스레드간 데이터 공유 (0) | 2024.10.31 |
[Java] 멀티스레딩 - 성능 최적화 (Feat : 스레드 풀) (0) | 2024.10.30 |
[Java] 멀티스레딩 - 스레드 조정 (0) | 2024.10.25 |