-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Labels
🐇 11 Concurrency11장 동시성11장 동시성
Description
Chapter : 11. 동시성
Item : 83. 지연 초기화는 신중히 사용하라
Assignee : jseok0917
🍑 서론
지연 초기화(Lazy initialization)
- 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
일반적인 초기화 vs 지연 초기화
- 일반적인 초기화
public class InitializationExample {
//즉시 초기화, 이 값을 바로 메모리에 할당
//하지만, 이 필드를 잘 사용하지 않는다면 메모리 낭비가 될 수 있다.
private Resource resource = new Resource();
public Resource getResource() {
return resource;
}
}- 지연 초기화
public class LazyInitializationExample {
private Resource resource; //선언만 해두고
//getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
public Resource getResource() {
//다만 해당 필드를 가져올때마다 계속 if문을 통한 검사로직이 필요 => 추가비용 발생
if (resource == null) {
resource = new Resource();
}
return resource;
}
}- 요약
- 일반적인 초기화 : 초기화 비용 O, 검사비용 없음, 코드단순
- 지연 초기화 : 초기화 비용 낮음, 검사비용 O, 코드복잡
- 대부분의 상황에서는 일반적인 초기화가 지연 초기화보다 낫다
- 지연 초기화를 사용할 때는 항상 성능테스트 해보고 도입해야한다.
🍑 본론
지연 초기화는 어떤 상황에 써야할까?
-
성능 최적화를 위해
- 필드의 초기화 비용이 크고, 필드를 낮은 빈도로 사용할 때
-
초기화 순환성을 피하기 위해 (아래 번외 참고, 책 443페이지에 번역오류있음, [Effective Java] Item 83. 지연 초기화로 인한 초기화 순환성의 영향 JavaBookStudy/JavaBook#45 참고)
- 멀티스레드 환경에서의 지연 초기화
- 지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면
- 한번만 초기화해야하는 필드를 두 개의 스레드가 동시에 초기화할 수 있으므로,
- 동시성 이슈에 대한 해결책이 필요하다.
어떻게 지연 초기화 코드를 짜야할까?
- 멀티스레드 환경을 고려
public class LazyInitializationExample {
private Resource resource; //선언만 해두고
//getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
//동시성을 고려하여 Synchronized 키워드로 작성
public synchronized Resource getResource() {
if (resource == null) {
resource = new Resource();
}
return resource;
}
}- 동시성 문제는 간단히 해결되지만 synchronized로 성능이 오히려 저하된다. 사실 초기화가 두번 이루어지는게 문제가 될 수 있는건데, 두번 초기화돼도 상관없는 상황이면 그냥 synchonized 떼고 써도됨(83-5 단일검사 관용구)
- 혹은, 성능에 관계없이 초기화 순환성을 피하기 위해 지연초기화를 사용하는 것이라면 그냥 이렇게 synchronized 를 사용하는게 명료한 답
- 지연 초기화 홀더 클래스 관용구 사용
- 자바에서 클래스는 클래스가 처음 쓰일 때 비로소 초기화된다는 점을 이용
public class LazyInitializationExample {
//정적 내부 클래스 사용
private static class ResourceHolder {
static final Resource INSTANCE = new Resource();
}
public Resource getResource() {
return ResourceHolder.INSTANCE;
}
}- synchronized 사용할 필요 아예 없음 => 성능저하X
- 클래스 초기화할 때는 VM이 알아서 필드접근에 대한 동기화 시켜줌
- 혹은 이중검사 관용구를 사용
public class LazyInitializationExample {
private volatile Resource resource; //선언만 해두고, volatile로!
//getter를 통해 실제로 해당 필드를 가져올 때 값이 null이면 할당해준다
//Synchronized 키워드를 메서드 내부 검사로직에 넣어준다
public Resource getResource() {
Resource result = this.resource;
//값이 이미 할당돼있는 상태면 바로 결과 반환(lock 사용X)
if (result != null) {
return result;
}
//초기화가 안돼있을때만 lock 사용
synchronized(this) {
if (result == null) {
result = new Resource();
}
return result;
}
}
}- 이미 초기화된 상태에서는 synchronized가 작동하지 않으므로 성능저하가 생기지 않음
- 필드를 volatile로 선언해야함(아이템78 참고)
🍑 결론
- 지연초기화는 항상 성능테스트 해보고 써야한다.
- 성능 문제로 쓰는거면 지연 초기화 홀더 클래스 관용구 사용하면 좋음
- 코드 ㅈㄴ복잡해짐 왜 왠만하면 일반적인 초기화가 낫다고 하는지 알겠죠?
번외
초기화 순환성이란?
- 클래스나 객체를 초기화할 때, 둘 이상의 요소가 서로를 참조해야하는 경우 발생할 수 있음
- StackOverFlow
public static class A {
private B b;
// A 객체 생성 시 A 인스턴스를 새로 만들기 때문에 문제가 발생한다.
public A() {
b = new B(new A()); // 새로운 A 인스턴스를 생성하며, 이로 인해 무한 루프가 발생
}
}
public static class B {
private A a;
public B(A a) {
this.a = a;
}
}
public static void main(String[] args) {
A a = new A(); // 무한 루프에 빠짐
}- 지연 초기화를 이용한 문제 해결
public static class A {
private B b;
public A() {
// 지연 초기화를 위해 B 인스턴스 생성을 미룸
}
//두번 초기화되는 문제를 해결하기 위해 synchronized
public synchronized B getB() {
if (b == null) {
b = new B(this); // 필요 시에만 B 인스턴스 생성
}
return b;
}
}
public static class B {
private A a;
public B(A a) {
this.a = a;
}
}
public static void main(String[] args) {
A a = new A(); // 무한 루프에 빠지지 않음
B b = a.getB(); // B 인스턴스가 처음 사용될 때 생성
}Referenced by
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
🐇 11 Concurrency11장 동시성11장 동시성