Lock을 사용하는 이유는 여러 Thread가 하나의 메서드에 동시 접근했을 때 발생할 수 있는 동시성 문제를 해결하기 위해 사용합니다.
예를 들어
private int count = 0;
public void addCount() {
count = count + 1;
}
이러한 로직이 있으며 아래 순서대로 쓰레드 2개가 해당 메서드를 호출합니다.
- [Thread-1] A 쓰레드가 addCount()에 접근하여 count:0 에 1을 더합니다.
- [Thread-2] B 쓰레드가 addCount()에 접근하여 count:0 에 1을 더합니다.
- [Thread-1] A 쓰레드가 0+1 의 결과값인 1을 count에 입력합니다.
- [Thread-2] B 쓰레드가 0+1 의 결과값인 1을 count에 입력합니다.
예상한 값은 A한번 B한번 총 2가 되어야 하는데 결과값은 1 로 동시성(경쟁 조건) 문제가 발생했습니다.
해당 로직을 해결하기 위해 Lock 을 추가하겠습니다.
private int count = 0;
synchronized public void addCount() {
count = count + 1;
}
synchronized라는 동기화 키워드를 삽입했고, AB가 동일한 로직을 접근했을 때에
- [Thread-1] A 쓰레드가 addCount()에 접근하여 count:0 에 1을 더합니다.
- [Thread-2] B 쓰레드가 addCount()에 접근하려 하지만 Lock이 걸려있으므로 '대기' 상태가 됩니다.
- [Thread-1] A 쓰레드가 0+1 의 결과값인 1을 count에 입력합니다.
- [Thread-2] B 쓰레드가 addCount()에 접근하여 count:1 에 1을 더합니다.
- [Thread-2] B 쓰레드가 1+1 의 결과값인 2를 count에 입력합니다.
동기화 로직을 추가해 동시성 문제를 해결했습니다.
Double check Lock에 대해서 보시죠
어디서 사용하는가? '객체를 하나만 생성 할때' and '객체 생성 비용이 클때'
HashCode 연산을 예로 들어보죠.
public class User {
private String name;
private int age;
@Override
public int hashCode() {
return super.hashCode(); // 해쉬코드 계산 비용이 많이 든다고 가정
}
}
User 의 Hash값을 캐싱하는 기능을 추가하면
public class User {
private String name;
private int age;
private Integer hash = null;
@Override
public int hashCode() {
if (hash == null) {
hash = super.hashCode(); // 캐싱
}
return hash;
}
}
이 코드 또한 A, B라는 쓰레드가 동시에 hashCode()라는 메서드에 접근한다면 A, B, 모두 super.hash(); 라는 메서드를 실행시키고
동일한 값을 hash에 적재할 것입니다.
그걸 해결하기 위해서 Lock을 추가해보면 다음과 같은 문제들이 발생합니다.
@Override
synchronized public int hashCode() {
if (hash == null) {
hash = super.hashCode();
}
return hash;
}
쓰레드 100개가 동시에 hashCode() 메서드를 실행하면,
super.hashCode() 연산은 하나의 쓰레드로만 처리하지만 쓰레드 99개가 모두 앞 쓰레드가 메서드를 빠져나올 때 까지 기다려야 하죠.
이러한 문제를 해결한 것이 Double Check Lock입니다.
1 @Override
2 public int hashCode() {
3 Integer result = hash;
4 if (result == null) {
5 synchronized(this) {
6 result = hash;
7 if (result == null) {
8 hash = result = super.hashCode();
9 }
10 }
11 }
12 return result;
13 }
우선 여러 쓰레드가 동시에 메서드로 접근 가능하고 hash 값이 null이 아닌경우 결과값을 리턴합니다.
hash가 null이면 syncronized블럭에 들어가고 hash값이 null인지 한번 더 확인합니다.
여러 쓰레드가 접근하는 순서는 풀어보면
- A 쓰레드 메서드 접근 { hash : null }
- B 쓰레드 메서드 접근 { hash : null }
- A 쓰레드 synchronized(this) 블럭 진입 { result : null, hash : null }
- B 쓰레드 synchronized(this) 블럭 대기 { result : null, hash : null }
- A 쓰레드 hash 값 생성 후 return { result : 21536, hash : 21536 }
- B 쓰레드 synchronized(this) 블럭 진입 후 6번째줄 실행 { result : 21536, hash : 21536 }
- B 쓰레드 7번째 줄 result 는 null이 아니므로 super.hashCode()를 실행하지 않고 return { result : 21536, hash : 21536 }
hash = null 인 상태로, B 쓰레드가 A 쓰레드와 메서드에 동시에 접근했으나,
B 쓰레드는 hashCode를 생성하지 않았으며.
100개의 요청이 동시에 들어와도 Lock이 걸리지 않아 속도가 빠르게 hash값을 return해줄 수 있습니다.
Double Check Lock은 여러므로 좋은 로직이지만 실무에서 사용해본적은 없습니다..
유지보수가 어렵고 아래처럼 대체로 사용할 수 있는 방법이 많기에 저는 굳이 사용하지는 않습니다.
- Initialization-on-demand holder idiom
- lazy initialization
- eager initialization
'Java & Kotlin' 카테고리의 다른 글
Java 불변 객체(Immutable Object) 와 장단점 (2) | 2024.02.04 |
---|---|
자바의 Garbage Collection 이해와 성능 최적화 방법 (0) | 2023.07.30 |
Java Functional Interface로 API 테스트 모듈 간결하게 만들기: 적용 사례와 코드 개선 (0) | 2023.07.11 |
Stack Overflow Error 발생 원인과 해결 방법: 재귀 호출 피하기 (0) | 2023.05.15 |
Java에서 제공해주는 Functional Interface (0) | 2023.04.30 |