با توجه به مستندات مربوطه کاتلین سه نوع پیادهسازی مختلف برای Lazy دارد
public enum class LazyThreadSafetyMode {
SYNCHRONIZED,
PUBLICATION,
NONE,
}
SYNCHRONIZED : تنها یک ترد میتواند آن را مقداردهی کند
PUBLICATION: متد مقداردهنده در شرایط همزمانی بین ترد ها ممکن است چندین مرتبه صدا شود. اما اولین مقداری که برگردانده شود بعنوان مقدار اصلی آن استفاده میشود.
NONE: هیچ محدودیتی برای سینکرونایز کردن متد مقداردهنده آن انجام نشده و رفتار آن در شرایط همزمانی مشخص نیست. تنها زمانی قابل استفادهاست که مطمئن باشیم تنها روی یک ترد مقداردهی خواهد شد.
همچنین نیاز به گفتن نیست که خود Lazy یک اینترفیس با یک متد و یک متغیر که و مقدار متغیر رو نگه میداره:
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
تنها پیادهسازی موجود در StdLib پیادهسازی مربوط به NONE میباشد که کدش اینه:
internal object UNINITIALIZED_VALUE
// internal to be called from lazy in JS
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)
}
internal class InitializedLazyImpl<out T>(override val value: T) : Lazy<T>, Serializable {
override fun isInitialized(): Boolean = true
override fun toString(): String = value.toString()
}
همونطور که میبینیم هیچ تدبیری برای تردسیف بودن مقداردهی متغیر مربوطه درنظر گرفته نشده و تنها درصورت UNINITIALIZED_VALUE بودن آن متد initializer آن را صدا میزند.
نکته جالب این پیادهسازی وجود متد writeReplace
که خروجی Any داره و private هم هست و هیچ جایی صدا زده نشده! پس کاربردش چیه؟
دلیلش توی Serializable وجود داره که توی لینک منبع ۲ توضیح داده شده.
این پیادهسازی با توجه به امکانات تردینگ موجود توی JVM انجام شده و همونطور که میبینیم با استفاده از یک lock فرآیند مقداردهی شدن متغیر داخلی را synchronize کرده که تنها یک ترد بتونه وارد بخش بعدی بشه
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)
}
نکته اول اینکه این پیادهسازی از خود این کلاس بعنوان lock استفاده میکنه.
و دیگری اینکه وقتی ما متغیری رو بصورت زیر تعریف میکنیم، از همین پیادهسازی Lazy استفاده میکنیم
که یعنی ترد سیفه و با خیال راحت میتونیم تو شرایط همزمانیم ازش استفاده کنیم
private val lazyVariable by lazy { /* ... */ }
// uses
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
پیادهسازی آخر هم مربوط به PUBLICATION عه که با رویکرد دیگهای نوشته شده و بجای سینکرونایز کردن بخش لاک کردن تردها برای ورود به بخش مقداردهی، از توابع java.util.concurrent.atomic برای اتمیک کردن و به یکباره مقداردهی شدن متغیر استفاده کرده.
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"
)
}
}
نکته قابل توجه این پیادهسازی هم استفاه از AtomicReferenceFieldUpdater عه که تو بخش companion object اومده و بقول منبع۲ یه روش reflection-base برای آپدیت یکباره فیلدهای volatile عه.
در نهایت هم بگم اینکه تنها مورد استفاده از
Lazy
محدود به
lazy { ... }
نمیشه و ما میتونیم
نوع تردسیف بودن رو مشخص کنیم
private val lazyVariable by lazy { /* ... */ }
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
یک فیلد Lazy با نوع سینکرونایزد میسازه و اگر موقع مقداردهی به exception بخوره دفعه بعدی که بخوایم بهش دسترسی پیدا کنیم مقداردهی رو مجدد انجام میده.
private val lazyVariable by lazy(LazyThreadSafetyMode.PUBLICATION) { /* ... */ }
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)
}
میتونیم نوع پیادهسازی دلخواهمون رو مشخص کنیم.
private val LOCK = Any()
private val lazyVariable by lazy(LOCK) { /* ... */ }
public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
این هم یک متد دیگه برای استفاده از پیادهسازی سینکرونایزه، با این تفاوت که میتونیم آبجکت lock رو خودمون بهش پاس بدیم. توی موارد قبلی و بصورت پیشفرض از خود ابجکت بعنوان lock استفاده میکرد