
이 둘의 차이점 중 동시성 처리때문에 사용처가 갈린다는 말을 듣고 한번 뜯어보려고 한다.
| 항목 | lazy | lateinit |
|---|---|---|
| 대상 | val (읽기 전용) | var (변경 가능) |
| 초기화 시점 | 처음 접근 시 (Lazy Evaluation) | 개발자가 명시적으로 초기화 |
| 스레드 안전성 | 기본적으로 스레드 안전 (LazyThreadSafetyMode.SYNCHRONIZED) | 스레드 안전하지 않음 |
| Nullable 가능 여부 | 가능 (val x: String? by lazy { ... }) | 불가 (lateinit var x: String? ❌) |
| 사용 용도 | 계산 비용이 크거나 나중에 필요할 때 | 의존성 주입, 테스트용 객체 등 나중에 값을 지정해야 할 때 |
| 초기화 검사 | 자동으로 초기화 여부 체크 | .isInitialized로 수동 체크 필요 |
lazy는 총 세가지 모드를 가지고 있으며 기본값이 thread-safe한 LazyThreadSafetyMode.SYNCHRONIZED를 사용하고있다.
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
lazy의 내부 구현을 확인해보면 아래 이미지처럼 synchronized와 lock을 이용해서 동기화를 하고있는걸 확인할 수 있다.
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
위에 나온 방식은 sync를 사용해 무조건 락으로 오버헤드가 발생한다. 반면 PUBLICATION 방식은 락은 없지만 동시성을 해결하고싶을 때 사용한다고 한다.
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
@Volatile private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// this final field is required to enable safe initialization of the constructed instance
private val final: Any = UNINITIALIZED_VALUE
override val value: T
get() {
val value = _value
if (value !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return value as T
}
val initializerValue = initializer
// if we see null in initializer here, it means that the value is already set by another thread
if (initializerValue != null) {
val newValue = initializerValue()
if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
initializer = null
return newValue
}
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
companion object {
private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
SafePublicationLazyImpl::class.java,
Any::class.java,
"_value"
)
}
}
value의 getter를 보면 락이 없고 compareAndSet을 통해 초기화를 시도하고 성공하면 해당 값을, 실패하면 기존에 할당된 값을 사용하게 된다.
락이 없는 만큼 초기화 부분이 여러번 일어날 수 있으며 최초에 할당된 값을 사용하게 된다.
아름에서도 알 수 있듯이 thread-safe하지 않는 lazy를 생성하게 된다.
동기화 관련 처리가 필요없으면서 lazy를 사용하게된다면 사용해서 오버헤드를 줄이면 좋을 것 같다.
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE
override val value: T
get() {
if (_value === UNINITIALIZED_VALUE) {
_value = initializer!!()
initializer = null
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}