지연 초기화
- 필드의 초기화 시점을 해당 값이 처음 필요할 때까지 늦추는 기법
- 값이 전혀 쓰이지 않을 경우 초기화가 일어나지 않음
- 정적 필드와 인스턴스 필드 모두 적용 가능
- 주로 최적화 용도로 쓰이지만, 클래스와 인스턴스 초기화 시 발생하는 `위험한 순환` 문제를 해결하는 효과도 있음
- 위험한 순환(Perilous Cycle) 문제는 초기화하는 동안 객체가 자기 자신을 참조하는 경우에 발생할 수 있는 문제
- ex) 클래스나 객체의 생성자에서 다른 객체를 생성하고, 이 생성된 객체가 다시 자신을 참조하는 경우 위험한 순환 문제가 발생 (무한 루프)
1. 지연 초기화는 필요할 때까지는 하지 말자
- 지연 초기화는 양날의 검
- 클래스 혹은 인스턴스 생성 시 발생하는 초기화 비용은 줄지만 지연 초기화하는 필드에 접근하는 비용이 커짐
- 지연 초기화하려는 필드들 중 다음과 같은 상황에 따라 오히려 지연 초기화로 인해 성능 저하가 발생할 수 있음
- 지연 초기화가 이루어지는 비율
- 실제 초기화에 드는 비용
- 초기화된 각 필드 호출 빈도
- 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 나음
2. 지연 초기화가 필요한 케이스
- `클래스의 인스턴스 중 해당 필드를 사용하는 인스턴스의 비율이 낮은 반면 해당 필드를 초기화하는 비용이 클 경우` 지연 초기화가 제 역할을 할 확률이 높음
- 지연 초기화 적용 전후 성능 비교 반드시 진행할 것
3. 멀티 쓰레드 환경에서의 지연 초기화
- 지연 초기화하는 필드를 둘 이상의 쓰레드가 공유할 경우 어떤 형태로든 반드시 동기화 필요
- 동기화하지 않을 경우 심각한 버그를 야기할 수 있음
인스턴스 필드 초기화하는 방법
1. 일반적인 방법
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private final FieldType field1 = computeFieldValue(); |
코드 부연 설명
- final 한정자를 사용했고
- 인스턴스를 생성함과 동시에 초기화
2. Synchronized 접근자 방식
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private FieldType field2; | |
private synchronized FieldType getField2() { | |
if (field2 == null) { | |
field2 = computeFieldValue(); | |
} | |
return field2; | |
} |
코드 부연 설명
- 지연 초기화가 초기화 순환성을 깨뜨릴 수 있다면 synchronized 접근자를 사용
- `초기화 순환성`은 서로가 서로를 초기화해야 하는 경우
- 정적 필드와 인스턴스 필드 모두 적용 가능
초기화 순환성 예시
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ClassA { | |
private ClassB b; | |
public ClassA() { | |
b = new ClassB(); | |
} | |
} | |
public class ClassB { | |
private ClassA a; | |
public ClassB() { | |
a = new ClassA(); | |
} | |
} |
3. 지연 초기화 홀더 클래스 관용구(Lazy Initialization Holder Class)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static class FieldHolder { | |
static final FieldType field = computeFieldValue(); | |
} | |
private static FieldType getField() { | |
return FieldHolder.field; | |
} |
코드 부연 설명
- 성능 때문에 정적 필드를 지연 초기화해야 할 경우 지연 초기화 홀더 클래스 관용구를 사용
- `클래스는 클래스가 처음 쓰일 때 비로소 초기화`된다는 특성을 이용한 관용구
- getField() 메서드가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서 비로소 FieldHodler 클래스 초기화를 진행
- 지연 초기화 홀더 클래스 관용구의 장점은 getField() 메서드가 필드에 접근할 때 동기화를 전혀 사용하지 않으므로 성능 저하가 없다는 점
- 일반적인 VM은 오직 클래스를 초기화할 때만 필드 접근을 동기화
- 클래스 초기화가 끝나면 VM이 동기화 코드를 제거하여 그다음부터는 아무런 검사나 동기화 없이 필드에 접근
4. 이중 검사 관용구 (Double-Check)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private volatile FieldType field4; | |
// NOTE: The code for this method in the first printing had a serious error (see errata for details)! | |
private FieldType getField4() { | |
FieldType result = field4; | |
if (result != null) { // First check (no locking) | |
return result; | |
} | |
synchronized (this) { | |
if (field4 == null) {// Second check (with locking) | |
field4 = computeFieldValue(); | |
} | |
return field4; | |
} | |
} |
코드 부연 설명
- 성능 떄문에 인스턴스 필드를 지연 초기화해야 할 경우 이중 검사 관용구를 사용
- 해당 관용구는 초기화된 필드에 접근할 때의 동기화 비용을 제거
- 필드의 값을 두 번 검사하는 방식으로 한 번은 동기화 없이 검사하고 필드가 아직 초기화되지 않았을 경우 두 번째는 동기화하여 검사
- 두 번째 검사에서도 필드가 초기화되지 않았을 때만 필드를 초기화
- 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언하여 캐시가 아닌 메모리에서 읽어오도록 처리 필요
- result 변수는 필드가 이미 초기화된 상황에서는 해당 필드를 딱 한 번만 읽도록 보장하는 역할
- 반드시 필요하지 않지만 성능을 높여주고 저수준 동시성 프로그래밍에 표준적으로 적용되는 방법
- 정적 필드에도 이중 검사 관용구를 적용할 수 있지만 앞서 설명한 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하는 것을 권장
5. 단일 검사 관용구 (Single-Check)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private volatile FieldType field5; | |
private FieldType getField5() { | |
FieldType result = field5; | |
if (result == null) { | |
field5 = result = computeFieldValue(); | |
} | |
return result; | |
} |
코드 부연 설명
- 반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할 경우가 존재하는데 이런 케이스라면 이중검사 관용구에서 두 번째 검사를 생략 가능
- 하지만 필드는 여전히 volatile로 선언하여 동기화 비용을 제거
1번부터 5번 초기화 기법 간단 정리
- 인스턴스 필드 초기화하는 1번부터 5번까지의 방법은 기본 타입 필드와 객체 참조 필드 모두에 적용 가능
- 이중 검사와 단일 검사 관용구를 수치 기본 타입 필드에 적용할 경우 필드의 값을 null 대신 숫자 기본 타입 변수의 기본값인 0과 비교하면 됨
6. 짜릿한 단일검사 관용구(Racy Single-Check)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RacySingleCheck { | |
private int sharedValue = 0; // 다른 기본 타입을 사용하는 필드 | |
// 필드 값 업데이트 메서드 | |
public void updateValue(int newValue) { | |
sharedValue = newValue; // 필드 값을 업데이트함 | |
} | |
// 필드 값 읽기 메서드 | |
public int getValue() { | |
return sharedValue; // 필드 값을 반환함 | |
} | |
public static void main(String[] args) { | |
RacySingleCheck example = new RacySingleCheck(); | |
// 쓰레드 생성 | |
Thread thread1 = new Thread(() -> { | |
example.updateValue(1); | |
}); | |
Thread thread2 = new Thread(() -> { | |
example.updateValue(2); | |
}); | |
thread1.start(); | |
thread2.start(); | |
try { | |
thread1.join(); | |
thread2.join(); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
System.out.println("필드 값: " + example.getValue()); | |
} | |
} |
코드 부연 설명
- 모든 쓰레드가 필드의 값을 다시 계산해도 상관없고 필드의 타입이 long과 double을 제외한 다른 기본 타입일 경우 단일 검사의 필드 선언에서 volatile 한정자를 없애도 됨
- 단일 검사 관용구에서 volatile 한정자를 없앤 관용구를 짜릿한 단일 검사 관용구라고 지칭
- 필드 값을 업데이트하는 메서드인 updateValue()는 단일 연산으로 이루어지므로, 여러 쓰레드가 동시에 이 메서드를 호출해도 안전하게 작동
- 아주 이례적인 기법으로 보통은 쓰이지 않음
정리
- 대부분의 필드는 지연시키지 말고 바로 초기화하는 것을 권장
- 성능 때문에 혹은 위험한 초기화 순환을 막기 위해서 지연초기화를 사용한다면 올바르게 사용해야 하며 1번부터 5번 방법 중 하나를 적용하는 것을 권장
- 인스턴스 필드에는 이중 검사 관용구를 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하는 것을 권장
- 반복해서 초기화해도 괜찮은 인스턴스에는 단일 검사 관용구도 고려해 보는 것을 권장
참고
이펙티브 자바
반응형
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 85] 자바 직렬화의 대안을 찾으라 (1) | 2024.05.12 |
---|---|
[아이템 84] 프로그램의 동작을 쓰레드 스케줄러에 기대지 말라 (0) | 2024.04.22 |
[아이템 82] 쓰레드 안전성 수준을 문서화하라 (0) | 2024.04.07 |
[아이템 81] wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2024.04.07 |
[아이템 80] 쓰레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2024.04.07 |